From bc154a0f4091ba99915e56dddf3d9e739d592a67 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 2 May 2026 14:30:07 +0000
Subject: [PATCH 01/43] Initial plan
From 8c7f1e2b8fd757cf53e5177ca0a57f378617a111 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 2 May 2026 14:41:29 +0000
Subject: [PATCH 02/43] =?UTF-8?q?NuGet=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=82=B9=E3=83=88=E3=82=A2=E3=81=AE=E5=9F=BA=E6=9C=AC?=
=?UTF-8?q?=E5=AE=9F=E8=A3=85=E3=82=92=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/Freeesia/WindowTranslator/sessions/5fefad82-801f-4158-ad6f-9d7500bf50ed
Co-authored-by: Freeesia <9002657+Freeesia@users.noreply.github.com>
---
.../Modules/PluginStore/NuGetPluginService.cs | 393 ++++++++++++++++++
.../Modules/PluginStore/PluginStoreView.xaml | 252 +++++++++++
.../PluginStore/PluginStoreView.xaml.cs | 80 ++++
.../PluginStore/PluginStoreViewModel.cs | 264 ++++++++++++
.../Modules/Settings/AllSettingsDialog.xaml | 4 +
.../Modules/Settings/AllSettingsViewModel.cs | 5 +
WindowTranslator/Program.cs | 6 +
.../Properties/Resources.Designer.cs | 311 +++++++++-----
WindowTranslator/Properties/Resources.ar.resx | 51 +++
WindowTranslator/Properties/Resources.cs.resx | 51 +++
WindowTranslator/Properties/Resources.de.resx | 51 +++
WindowTranslator/Properties/Resources.en.resx | 51 +++
WindowTranslator/Properties/Resources.es.resx | 51 +++
WindowTranslator/Properties/Resources.fa.resx | 51 +++
.../Properties/Resources.fil.resx | 51 +++
WindowTranslator/Properties/Resources.fr.resx | 51 +++
WindowTranslator/Properties/Resources.hi.resx | 51 +++
WindowTranslator/Properties/Resources.id.resx | 51 +++
WindowTranslator/Properties/Resources.ko.resx | 51 +++
WindowTranslator/Properties/Resources.ms.resx | 51 +++
WindowTranslator/Properties/Resources.pl.resx | 51 +++
.../Properties/Resources.pt-BR.resx | 51 +++
WindowTranslator/Properties/Resources.resx | 51 +++
WindowTranslator/Properties/Resources.ru.resx | 51 +++
WindowTranslator/Properties/Resources.th.resx | 51 +++
WindowTranslator/Properties/Resources.tr.resx | 51 +++
WindowTranslator/Properties/Resources.vi.resx | 51 +++
.../Properties/Resources.zh-CN.resx | 51 +++
.../Properties/Resources.zh-TW.resx | 51 +++
29 files changed, 2273 insertions(+), 113 deletions(-)
create mode 100644 WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
create mode 100644 WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
create mode 100644 WindowTranslator/Modules/PluginStore/PluginStoreView.xaml.cs
create mode 100644 WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
new file mode 100644
index 00000000..3f0c81bb
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -0,0 +1,393 @@
+using System.IO;
+using System.IO.Compression;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.Logging;
+
+namespace WindowTranslator.Modules.PluginStore;
+
+///
+/// NuGet V3 REST APIを使用してプラグインパッケージの検索・インストール・管理を行うサービスです。
+///
+public sealed class NuGetPluginService : IDisposable
+{
+ private const string NuGetServiceIndexUrl = "https://api.nuget.org/v3/index.json";
+ private const string PluginTag = "windowtranslator-plugin";
+ private const string NuGetFlatContainerBase = "https://api.nuget.org/v3-flatcontainer";
+
+ private static readonly string UserPluginsDir = Path.Combine(PathUtility.UserDir, "plugins");
+ private static readonly string ManifestPath = Path.Combine(UserPluginsDir, "nuget-manifest.json");
+
+ private static readonly JsonSerializerOptions JsonOptions = new()
+ {
+ PropertyNameCaseInsensitive = true,
+ AllowTrailingCommas = true,
+ WriteIndented = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ };
+
+ private readonly HttpClient httpClient;
+ private readonly ILogger logger;
+ private string? searchUrl;
+
+ public NuGetPluginService(ILogger logger)
+ {
+ this.httpClient = new HttpClient();
+ this.logger = logger;
+ }
+
+ ///
+ /// NuGetでWindowTranslatorプラグインを検索します。
+ ///
+ public async Task> SearchPackagesAsync(CancellationToken cancellationToken = default)
+ {
+ if (this.searchUrl is null)
+ {
+ this.searchUrl = await GetSearchUrlAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ var url = $"{this.searchUrl}?q=tags:{PluginTag}&take=100&semVerLevel=2.0.0&prerelease=false";
+ this.logger.LogDebug("NuGet検索URL: {Url}", url);
+
+ var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var result = await JsonSerializer.DeserializeAsync(content, JsonOptions, cancellationToken).ConfigureAwait(false)
+ ?? throw new InvalidOperationException("NuGet検索結果のデシリアライズに失敗しました。");
+
+ this.logger.LogInformation("NuGet検索完了: {Count}件のパッケージが見つかりました。", result.Data?.Length ?? 0);
+
+ return result.Data?.Select(d => new NuGetPackageInfo(
+ Id: d.PackageId ?? string.Empty,
+ Version: d.Version ?? string.Empty,
+ Title: d.Title ?? d.PackageId ?? string.Empty,
+ Description: d.Description ?? string.Empty,
+ Authors: string.Join(", ", d.Authors ?? []),
+ ProjectUrl: d.ProjectUrl,
+ LicenseUrl: d.LicenseUrl
+ )).ToArray() ?? [];
+ }
+
+ ///
+ /// 指定したNuGetパッケージをインストールします。
+ ///
+ public async Task InstallPackageAsync(string packageId, string version, IProgress? progress = null, CancellationToken cancellationToken = default)
+ {
+ var packageIdLower = packageId.ToLowerInvariant();
+ var versionLower = version.ToLowerInvariant();
+ var nupkgUrl = $"{NuGetFlatContainerBase}/{packageIdLower}/{versionLower}/{packageIdLower}.{versionLower}.nupkg";
+
+ this.logger.LogInformation("パッケージをダウンロード中: {PackageId} {Version}", packageId, version);
+
+ // 一時ディレクトリにダウンロード
+ var tempDir = Path.Combine(Path.GetTempPath(), "WindowTranslatorPlugins", packageId);
+ Directory.CreateDirectory(tempDir);
+ var tempNupkgPath = Path.Combine(tempDir, $"{packageIdLower}.{versionLower}.nupkg");
+
+ try
+ {
+ await DownloadFileAsync(nupkgUrl, tempNupkgPath, progress, cancellationToken).ConfigureAwait(false);
+
+ // ターゲットディレクトリを準備
+ var targetDir = Path.Combine(UserPluginsDir, packageId);
+ // 古いファイルをバックアップして削除する前に一時フォルダへ移動
+ var backupDir = $"{targetDir}.backup";
+ if (Directory.Exists(targetDir))
+ {
+ if (Directory.Exists(backupDir))
+ Directory.Delete(backupDir, recursive: true);
+ Directory.Move(targetDir, backupDir);
+ }
+
+ Directory.CreateDirectory(targetDir);
+
+ try
+ {
+ // nupkgを展開して必要なDLLをコピー
+ ExtractPluginDlls(tempNupkgPath, targetDir);
+ this.logger.LogInformation("パッケージの展開完了: {PackageId} -> {TargetDir}", packageId, targetDir);
+ }
+ catch
+ {
+ // 失敗したら元に戻す
+ Directory.Delete(targetDir, recursive: true);
+ if (Directory.Exists(backupDir))
+ Directory.Move(backupDir, targetDir);
+ throw;
+ }
+
+ // バックアップを削除
+ if (Directory.Exists(backupDir))
+ Directory.Delete(backupDir, recursive: true);
+
+ // マニフェストを更新
+ await UpdateManifestAsync(packageId, version, cancellationToken).ConfigureAwait(false);
+ }
+ finally
+ {
+ // 一時ファイルを削除
+ try { File.Delete(tempNupkgPath); } catch { /* ignore */ }
+ }
+ }
+
+ ///
+ /// 指定したパッケージをアンインストールします。(次回起動時に適用)
+ ///
+ public async Task UninstallPackageAsync(string packageId, CancellationToken cancellationToken = default)
+ {
+ this.logger.LogInformation("パッケージをアンインストール: {PackageId}", packageId);
+
+ var targetDir = Path.Combine(UserPluginsDir, packageId);
+ // 実行中のDLLがロックされている可能性があるため、削除マーカーを置く
+ var pendingDeleteMarker = Path.Combine(UserPluginsDir, $"{packageId}.pending-delete");
+ await File.WriteAllTextAsync(pendingDeleteMarker, packageId, cancellationToken).ConfigureAwait(false);
+
+ // マニフェストから削除
+ await RemoveFromManifestAsync(packageId, cancellationToken).ConfigureAwait(false);
+
+ this.logger.LogInformation("パッケージ {PackageId} をアンインストールキューに追加しました。再起動後に完全に削除されます。", packageId);
+ }
+
+ ///
+ /// アプリ起動時にペンディング削除マーカーを処理します。
+ ///
+ public void ProcessPendingDeletions()
+ {
+ if (!Directory.Exists(UserPluginsDir))
+ return;
+
+ foreach (var markerFile in Directory.GetFiles(UserPluginsDir, "*.pending-delete"))
+ {
+ try
+ {
+ var packageId = File.ReadAllText(markerFile);
+ var targetDir = Path.Combine(UserPluginsDir, packageId);
+ if (Directory.Exists(targetDir))
+ {
+ Directory.Delete(targetDir, recursive: true);
+ this.logger.LogInformation("ペンディング削除を処理: {PackageId}", packageId);
+ }
+ File.Delete(markerFile);
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogWarning(ex, "ペンディング削除の処理に失敗: {MarkerFile}", markerFile);
+ }
+ }
+ }
+
+ ///
+ /// インストール済みのパッケージ一覧を取得します。
+ ///
+ public async Task> GetInstalledPackagesAsync(CancellationToken cancellationToken = default)
+ {
+ var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ return manifest.Packages;
+ }
+
+ private async Task GetSearchUrlAsync(CancellationToken cancellationToken)
+ {
+ var response = await this.httpClient.GetAsync(NuGetServiceIndexUrl, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var index = await JsonSerializer.DeserializeAsync(content, JsonOptions, cancellationToken).ConfigureAwait(false)
+ ?? throw new InvalidOperationException("NuGetサービスインデックスのデシリアライズに失敗しました。");
+
+ var searchEntry = index.Resources?.FirstOrDefault(r => r.Type == "SearchQueryService/3.5.0")
+ ?? index.Resources?.FirstOrDefault(r => r.Type?.StartsWith("SearchQueryService", StringComparison.Ordinal) == true)
+ ?? throw new InvalidOperationException("NuGet検索サービスURLが見つかりませんでした。");
+
+ return searchEntry.Id ?? throw new InvalidOperationException("NuGet検索サービスURLが空です。");
+ }
+
+ private async Task DownloadFileAsync(string url, string destPath, IProgress? progress, CancellationToken cancellationToken)
+ {
+ using var response = await this.httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ var totalBytes = response.Content.Headers.ContentLength ?? -1;
+ var downloadedBytes = 0L;
+
+ using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ using var fileStream = File.Create(destPath);
+ var buffer = new byte[81920];
+ int bytesRead;
+ while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0)
+ {
+ await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
+ downloadedBytes += bytesRead;
+ if (totalBytes > 0)
+ {
+ progress?.Report((double)downloadedBytes / totalBytes);
+ }
+ }
+ }
+
+ private static void ExtractPluginDlls(string nupkgPath, string targetDir)
+ {
+ using var archive = ZipFile.OpenRead(nupkgPath);
+
+ // 最適なTFMのlib/エントリを探す
+ var libEntries = archive.Entries
+ .Where(e => e.FullName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(e.Name)
+ && e.Name != "_._")
+ .ToList();
+
+ if (!libEntries.Any())
+ {
+ throw new InvalidOperationException("パッケージにlib/フォルダが見つかりませんでした。");
+ }
+
+ // TFMを選択(net10.0-windows > net10.0 > net9.0-windows > net9.0 > ... の優先順位)
+ var tfmGroups = libEntries
+ .GroupBy(e => e.FullName.Split('/')[1])
+ .ToList();
+
+ var selectedTfm = SelectBestTfm([.. tfmGroups.Select(g => g.Key)]);
+ if (selectedTfm is null)
+ {
+ throw new InvalidOperationException("互換性のあるターゲットフレームワークが見つかりませんでした。");
+ }
+
+ var selectedEntries = tfmGroups.First(g => g.Key == selectedTfm);
+
+ foreach (var entry in selectedEntries)
+ {
+ var destPath = Path.Combine(targetDir, entry.Name);
+ entry.ExtractToFile(destPath, overwrite: true);
+ }
+ }
+
+ private static string? SelectBestTfm(string[] tfms)
+ {
+ // TFMの優先度リスト(.NET 10から降順、Windows版を優先)
+ var orderedPrefixes = new[]
+ {
+ "net10.0-windows",
+ "net10.0",
+ "net9.0-windows",
+ "net9.0",
+ "net8.0-windows",
+ "net8.0",
+ "net7.0-windows",
+ "net7.0",
+ "net6.0-windows",
+ "net6.0",
+ "netstandard2.1",
+ "netstandard2.0",
+ };
+
+ foreach (var prefix in orderedPrefixes)
+ {
+ // 完全一致または前方一致(例: net10.0-windows10.0.20348.0)
+ var match = tfms.OrderByDescending(t => t).FirstOrDefault(t =>
+ t.Equals(prefix, StringComparison.OrdinalIgnoreCase)
+ || t.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase)
+ || t.StartsWith(prefix + "_", StringComparison.OrdinalIgnoreCase));
+ if (match is not null)
+ return match;
+ }
+
+ return tfms.FirstOrDefault();
+ }
+
+ private async Task UpdateManifestAsync(string packageId, string version, CancellationToken cancellationToken)
+ {
+ var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ var packages = manifest.Packages.ToList();
+ var existing = packages.FindIndex(p => p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase));
+ var newEntry = new InstalledPackageInfo(packageId, version, DateTime.UtcNow);
+ if (existing >= 0)
+ packages[existing] = newEntry;
+ else
+ packages.Add(newEntry);
+
+ await SaveManifestAsync(new InstalledManifest([.. packages]), cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task RemoveFromManifestAsync(string packageId, CancellationToken cancellationToken)
+ {
+ var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ var packages = manifest.Packages.Where(p => !p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)).ToList();
+ await SaveManifestAsync(new InstalledManifest([.. packages]), cancellationToken).ConfigureAwait(false);
+ }
+
+ private async Task LoadManifestAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ if (File.Exists(ManifestPath))
+ {
+ using var fs = File.OpenRead(ManifestPath);
+ var manifest = await JsonSerializer.DeserializeAsync(fs, JsonOptions, cancellationToken).ConfigureAwait(false);
+ return manifest ?? new InstalledManifest([]);
+ }
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogWarning(ex, "プラグインマニフェストの読み込みに失敗しました。");
+ }
+ return new InstalledManifest([]);
+ }
+
+ private static async Task SaveManifestAsync(InstalledManifest manifest, CancellationToken cancellationToken)
+ {
+ Directory.CreateDirectory(UserPluginsDir);
+ using var fs = File.Create(ManifestPath);
+ await JsonSerializer.SerializeAsync(fs, manifest, JsonOptions, cancellationToken).ConfigureAwait(false);
+ }
+
+ public void Dispose()
+ {
+ this.httpClient.Dispose();
+ }
+}
+
+/// NuGetパッケージ情報
+public record NuGetPackageInfo(
+ string Id,
+ string Version,
+ string Title,
+ string Description,
+ string Authors,
+ string? ProjectUrl,
+ string? LicenseUrl
+);
+
+/// インストール済みパッケージ情報
+public record InstalledPackageInfo(
+ string Id,
+ string Version,
+ DateTime InstalledAt
+);
+
+/// インストール済みパッケージのマニフェスト
+public record InstalledManifest(List Packages);
+
+// NuGet V3 API レスポンス型
+internal record NuGetServiceIndex(
+ [property: JsonPropertyName("resources")] NuGetServiceResource[]? Resources
+);
+
+internal record NuGetServiceResource(
+ [property: JsonPropertyName("@id")] string? Id,
+ [property: JsonPropertyName("@type")] string? Type
+);
+
+internal record NuGetSearchResponse(
+ [property: JsonPropertyName("totalHits")] int TotalHits,
+ [property: JsonPropertyName("data")] NuGetSearchData[]? Data
+);
+
+internal record NuGetSearchData(
+ [property: JsonPropertyName("id")] string? PackageId,
+ [property: JsonPropertyName("version")] string? Version,
+ [property: JsonPropertyName("title")] string? Title,
+ [property: JsonPropertyName("description")] string? Description,
+ [property: JsonPropertyName("authors")] string[]? Authors,
+ [property: JsonPropertyName("projectUrl")] string? ProjectUrl,
+ [property: JsonPropertyName("licenseUrl")] string? LicenseUrl
+);
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
new file mode 100644
index 00000000..4b26ed4b
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
@@ -0,0 +1,252 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml.cs b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml.cs
new file mode 100644
index 00000000..956ef27b
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml.cs
@@ -0,0 +1,80 @@
+using System.Globalization;
+using System.Windows;
+using System.Windows.Data;
+
+namespace WindowTranslator.Modules.PluginStore;
+
+///
+/// PluginStoreView.xaml の相互作用ロジック
+///
+public partial class PluginStoreView
+{
+ private bool loaded;
+
+ public PluginStoreView()
+ {
+ InitializeComponent();
+ this.IsVisibleChanged += OnIsVisibleChanged;
+ }
+
+ private void OnIsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
+ {
+ if (this.loaded || !this.IsVisible)
+ return;
+ this.loaded = true;
+ if (this.DataContext is PluginStoreViewModel vm)
+ {
+ _ = vm.LoadCommand.ExecuteAsync(null);
+ }
+ }
+}
+
+///
+/// null でない場合に true を返すコンバーター
+///
+[ValueConversion(typeof(object), typeof(bool))]
+public sealed class NotNullToBoolConverter : IValueConverter
+{
+ public static NotNullToBoolConverter Default { get; } = new();
+
+ public object Convert(object? value, Type targetType, object parameter, CultureInfo culture)
+ => value is not null;
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ => throw new NotSupportedException();
+}
+
+///
+/// null でない場合に Visible を返すコンバーター
+///
+[ValueConversion(typeof(object), typeof(Visibility))]
+public sealed class NotNullToVisibilityConverter : IValueConverter
+{
+ public static NotNullToVisibilityConverter Default { get; } = new();
+
+ public object Convert(object? value, Type targetType, object parameter, CultureInfo culture)
+ => value is not null ? Visibility.Visible : Visibility.Collapsed;
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ => throw new NotSupportedException();
+}
+
+///
+/// bool を反転するコンバーター(Visibility対応)
+///
+[ValueConversion(typeof(bool), typeof(object))]
+public sealed class InverseBoolConverter : IValueConverter
+{
+ public static InverseBoolConverter Default { get; } = new();
+
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ var inverseBool = value is bool b && !b;
+ if (parameter is string p && p == "Visibility")
+ return inverseBool ? Visibility.Visible : Visibility.Collapsed;
+ return inverseBool;
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ => value is bool b && !b;
+}
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
new file mode 100644
index 00000000..527e005a
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -0,0 +1,264 @@
+using System.Collections.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Extensions.Logging;
+using WindowTranslator.Properties;
+using Wpf.Ui;
+using Wpf.Ui.Extensions;
+
+namespace WindowTranslator.Modules.PluginStore;
+
+///
+/// プラグインストアのViewModel
+///
+public partial class PluginStoreViewModel : ObservableObject
+{
+ private readonly NuGetPluginService nugetService;
+ private readonly ILogger logger;
+ private readonly IContentDialogService dialogService;
+ private readonly ISnackbarService snackbarService;
+
+ [ObservableProperty]
+ private bool isLoading;
+
+ [ObservableProperty]
+ private string? errorMessage;
+
+ [ObservableProperty]
+ private PluginPackageViewModel? selectedPackage;
+
+ public ObservableCollection Packages { get; } = [];
+
+ public PluginStoreViewModel(
+ NuGetPluginService nugetService,
+ ILogger logger,
+ IContentDialogService dialogService,
+ ISnackbarService snackbarService)
+ {
+ this.nugetService = nugetService;
+ this.logger = logger;
+ this.dialogService = dialogService;
+ this.snackbarService = snackbarService;
+ }
+
+ ///
+ /// プラグイン一覧を読み込みます。
+ ///
+ [RelayCommand]
+ public async Task LoadAsync(CancellationToken cancellationToken = default)
+ {
+ if (this.IsLoading)
+ return;
+
+ this.IsLoading = true;
+ this.ErrorMessage = null;
+
+ try
+ {
+ var installed = await this.nugetService.GetInstalledPackagesAsync(cancellationToken).ConfigureAwait(true);
+ var installedDict = installed.ToDictionary(p => p.Id, StringComparer.OrdinalIgnoreCase);
+
+ var packages = await this.nugetService.SearchPackagesAsync(cancellationToken).ConfigureAwait(true);
+ this.logger.LogInformation("NuGetから{Count}件のプラグインパッケージを取得しました。", packages.Count);
+
+ this.Packages.Clear();
+ foreach (var pkg in packages)
+ {
+ installedDict.TryGetValue(pkg.Id, out var installedInfo);
+ var isInstalled = installedInfo is not null;
+ var installedVersion = installedInfo?.Version;
+ var isUpdateAvailable = isInstalled
+ && installedVersion is not null
+ && IsNewerVersion(pkg.Version, installedVersion);
+
+ this.Packages.Add(new PluginPackageViewModel(pkg, isInstalled, installedVersion, isUpdateAvailable));
+ }
+
+ // インストール済みだがNuGetに見つからないパッケージも表示
+ foreach (var inst in installed)
+ {
+ if (!this.Packages.Any(p => p.Id.Equals(inst.Id, StringComparison.OrdinalIgnoreCase)))
+ {
+ this.Packages.Add(new PluginPackageViewModel(
+ new NuGetPackageInfo(inst.Id, inst.Version, inst.Id, string.Empty, string.Empty, null, null),
+ isInstalled: true,
+ installedVersion: inst.Version,
+ isUpdateAvailable: false));
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ // キャンセルは正常
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "NuGet検索に失敗しました。");
+ this.ErrorMessage = Resources.NuGetSearchFailed;
+ }
+ finally
+ {
+ this.IsLoading = false;
+ }
+ }
+
+ ///
+ /// プラグインをインストールまたは更新します。
+ ///
+ [RelayCommand]
+ public async Task InstallAsync(PluginPackageViewModel package)
+ {
+ package.IsInstalling = true;
+ try
+ {
+ this.logger.LogInformation("プラグインのインストール開始: {PackageId} {Version}", package.Id, package.LatestVersion);
+ var progress = new Progress(v => package.InstallProgress = v);
+ await this.nugetService.InstallPackageAsync(package.Id, package.LatestVersion, progress).ConfigureAwait(true);
+
+ package.IsInstalled = true;
+ package.InstalledVersion = package.LatestVersion;
+ package.IsUpdateAvailable = false;
+ package.InstallProgress = 0;
+
+ this.logger.LogInformation("プラグインのインストール完了: {PackageId}", package.Id);
+
+ // 再起動が必要な旨を表示
+ await this.dialogService.ShowSimpleDialogAsync(new()
+ {
+ Title = Resources.PluginInstallSuccess,
+ Content = Resources.RestartRequired,
+ CloseButtonText = Resources.Close,
+ }).ConfigureAwait(true);
+ }
+ catch (OperationCanceledException)
+ {
+ // キャンセルは正常
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "プラグインのインストールに失敗しました: {PackageId}", package.Id);
+ await this.dialogService.ShowAlertAsync(
+ Resources.PluginInstallFailed,
+ ex.Message,
+ Resources.Close).ConfigureAwait(true);
+ }
+ finally
+ {
+ package.IsInstalling = false;
+ }
+ }
+
+ ///
+ /// プラグインをアンインストールします。
+ ///
+ [RelayCommand]
+ public async Task UninstallAsync(PluginPackageViewModel package)
+ {
+ var result = await this.dialogService.ShowSimpleDialogAsync(new()
+ {
+ Title = Resources.Uninstall,
+ Content = string.Format(Resources.UninstallConfirm, package.Title),
+ PrimaryButtonText = Resources.Uninstall,
+ CloseButtonText = Resources.Cancel,
+ }).ConfigureAwait(true);
+
+ if (result != Wpf.Ui.Controls.ContentDialogResult.Primary)
+ return;
+
+ try
+ {
+ await this.nugetService.UninstallPackageAsync(package.Id).ConfigureAwait(true);
+ package.IsInstalled = false;
+ package.InstalledVersion = null;
+ package.IsUpdateAvailable = false;
+
+ await this.dialogService.ShowSimpleDialogAsync(new()
+ {
+ Title = Resources.Uninstall,
+ Content = Resources.RestartRequired,
+ CloseButtonText = Resources.Close,
+ }).ConfigureAwait(true);
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "プラグインのアンインストールに失敗しました: {PackageId}", package.Id);
+ await this.dialogService.ShowAlertAsync(
+ Resources.Uninstall,
+ ex.Message,
+ Resources.Close).ConfigureAwait(true);
+ }
+ }
+
+ private static bool IsNewerVersion(string latestVersion, string installedVersion)
+ {
+ try
+ {
+ return Version.Parse(latestVersion) > Version.Parse(installedVersion);
+ }
+ catch
+ {
+ return string.Compare(latestVersion, installedVersion, StringComparison.OrdinalIgnoreCase) > 0;
+ }
+ }
+}
+
+///
+/// プラグインパッケージの表示モデル
+///
+public partial class PluginPackageViewModel : ObservableObject
+{
+ public string Id { get; }
+ public string Title { get; }
+ public string Description { get; }
+ public string Authors { get; }
+ public string LatestVersion { get; }
+ public string? ProjectUrl { get; }
+ public string? LicenseUrl { get; }
+
+ [ObservableProperty]
+ private bool isInstalled;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(StatusText))]
+ private string? installedVersion;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(StatusText))]
+ private bool isUpdateAvailable;
+
+ [ObservableProperty]
+ private bool isInstalling;
+
+ [ObservableProperty]
+ private double installProgress;
+
+ public string StatusText
+ {
+ get
+ {
+ if (this.IsUpdateAvailable && this.InstalledVersion is not null)
+ return string.Format(Properties.Resources.UpdateAvailableVersion, this.InstalledVersion, this.LatestVersion);
+ if (this.IsInstalled && this.InstalledVersion is not null)
+ return string.Format(Properties.Resources.InstalledVersion, this.InstalledVersion);
+ return string.Empty;
+ }
+ }
+
+ public PluginPackageViewModel(
+ NuGetPackageInfo info,
+ bool isInstalled,
+ string? installedVersion,
+ bool isUpdateAvailable)
+ {
+ this.Id = info.Id;
+ this.Title = info.Title;
+ this.Description = info.Description;
+ this.Authors = info.Authors;
+ this.LatestVersion = info.Version;
+ this.ProjectUrl = info.ProjectUrl;
+ this.LicenseUrl = info.LicenseUrl;
+ this.isInstalled = isInstalled;
+ this.installedVersion = installedVersion;
+ this.isUpdateAvailable = isUpdateAvailable;
+ }
+}
diff --git a/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml b/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
index 890a733b..8c2eb942 100644
--- a/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
+++ b/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
@@ -6,6 +6,7 @@
xmlns:data="clr-namespace:WindowTranslator.Data"
xmlns:local="clr-namespace:WindowTranslator.Modules.Settings"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+ xmlns:pluginStore="clr-namespace:WindowTranslator.Modules.PluginStore"
xmlns:properties="clr-namespace:WindowTranslator.Properties"
xmlns:pt="http://propertytools.org/wpf"
xmlns:root="clr-namespace:WindowTranslator"
@@ -399,6 +400,9 @@
+
+
+
diff --git a/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs b/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
index 46d0641b..b7ecd468 100644
--- a/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
+++ b/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
@@ -19,6 +19,7 @@
using WindowTranslator.ComponentModel;
using WindowTranslator.Extensions;
using WindowTranslator.Modules.Main;
+using WindowTranslator.Modules.PluginStore;
using WindowTranslator.Properties;
using WindowTranslator.Stores;
using Wpf.Ui;
@@ -103,6 +104,8 @@ sealed partial class AllSettingsViewModel : ObservableObject, IDisposable
public bool IsVisibleReviewButton => this.reviewRequestService.CanOpenReview;
+ public PluginStoreViewModel PluginStore { get; }
+
public AllSettingsViewModel(
[Inject] PluginProvider provider,
[Inject] IOptionsSnapshot options,
@@ -116,6 +119,7 @@ public AllSettingsViewModel(
[Inject] IEnumerable validators,
[Inject] IMainWindowModule mainWindowModule,
[Inject] ILogger logger,
+ [Inject] PluginStoreViewModel pluginStoreViewModel,
string target,
bool? applyMode = null)
{
@@ -158,6 +162,7 @@ public AllSettingsViewModel(
this.logger = logger;
this.target = target;
this.rootConfig = config as IConfigurationRoot;
+ this.PluginStore = pluginStoreViewModel;
this.updateChecker.UpdateAvailable += UpdateChecker_UpdateAvailable;
SetUpUpdateInfo();
this.isStartup = GetIsStartup();
diff --git a/WindowTranslator/Program.cs b/WindowTranslator/Program.cs
index f2df1337..7296b8be 100644
--- a/WindowTranslator/Program.cs
+++ b/WindowTranslator/Program.cs
@@ -26,6 +26,7 @@
using WindowTranslator.Modules.ErrorReport;
using WindowTranslator.Modules.LogView;
using WindowTranslator.Modules.Main;
+using WindowTranslator.Modules.PluginStore;
using WindowTranslator.Modules.Settings;
using WindowTranslator.Modules.Startup;
using WindowTranslator.Modules.Validate;
@@ -161,6 +162,8 @@
builder.Services.AddPresentation();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddTransient();
builder.Services.Configure(builder.Configuration, op => op.ErrorOnUnknownConfiguration = false);
builder.Services.Configure(builder.Configuration.GetSection(nameof(UserSettings.Common)));
builder.Services.AddTransient(typeof(IConfigureNamedOptions<>), typeof(ConfigurePluginParam<>));
@@ -185,6 +188,9 @@
e.Window.Activate();
};
+// 起動時にペンディング削除を処理する
+app.Services.GetRequiredService().ProcessPendingDeletions();
+
if (SentrySdk.IsEnabled)
{
app.Logger.LogInformation("Sentry is enabled.");
diff --git a/WindowTranslator/Properties/Resources.Designer.cs b/WindowTranslator/Properties/Resources.Designer.cs
index 37d4cd4d..6ff2bf59 100644
--- a/WindowTranslator/Properties/Resources.Designer.cs
+++ b/WindowTranslator/Properties/Resources.Designer.cs
@@ -1,4 +1,4 @@
-//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
//
// This code was generated by a tool.
//
@@ -28,7 +28,7 @@
namespace WindowTranslator.Properties;
///
-/// [JCYꂽȂǂ邽߂́AɌ^w肳ꂽ\[X NXłB
+/// ローカライズされた文字列などを検索するための、厳密に型指定されたリソース クラスです。
///
// This class was auto-generated by a text template.
// To add or remove a member, edit your .ResX file.
@@ -45,15 +45,15 @@ internal Resources() {
}
///
- /// ̃NXŎgpĂLbVꂽ ResourceManager CX^XԂ܂B
+ /// このクラスで使用されているキャッシュされた ResourceManager インスタンスを返します。
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager
=> resourceMan ??= new CustomResourceManager("WindowTranslator.Properties.Resources", Assembly.GetExecutingAssembly());
///
- /// ׂĂɂāÃ݂Xbh CurrentUICulture vpeBI[o[Ch܂
- /// ݂̃Xbh CurrentUICulture vpeBI[o[Ch܂B
+ /// すべてについて、現在のスレッドの CurrentUICulture プロパティをオーバーライドします
+ /// 現在のスレッドの CurrentUICulture プロパティをオーバーライドします。
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture
@@ -63,542 +63,627 @@ internal Resources() {
}
///
- /// "̃Avɂ" ɗގĂ郍[JCYꂽ܂B
+ /// "このアプリについて" に類似しているローカライズされた文字列を検索します。
///
public static string About => ResourceManager.GetString("About", resourceCulture) ?? string.Empty;
///
- /// "A" ɗގĂ郍[JCYꂽ܂B
+ /// "連絡先" に類似しているローカライズされた文字列を検索します。
///
public static string Address => ResourceManager.GetString("Address", resourceCulture) ?? string.Empty;
///
- /// "Av" ɗގĂ郍[JCYꂽ܂B
+ /// "アプリ情報" に類似しているローカライズされた文字列を検索します。
///
public static string Application => ResourceManager.GetString("Application", resourceCulture) ?? string.Empty;
///
- /// "Kp" ɗގĂ郍[JCYꂽ܂B
+ /// "適用" に類似しているローカライズされた文字列を検索します。
///
public static string Apply => ResourceManager.GetString("Apply", resourceCulture) ?? string.Empty;
///
- /// "A^b`" ɗގĂ郍[JCYꂽ܂B
+ /// "アタッチ" に類似しているローカライズされた文字列を検索します。
///
public static string Attach => ResourceManager.GetString("Attach", resourceCulture) ?? string.Empty;
///
- /// "A^b`" ɗގĂ郍[JCYꂽ܂B
+ /// "アタッチ中" に類似しているローカライズされた文字列を検索します。
///
public static string Attaching => ResourceManager.GetString("Attaching", resourceCulture) ?? string.Empty;
///
- /// "XN[" ɗގĂ郍[JCYꂽ܂B
+ /// "自動スクロール" に類似しているローカライズされた文字列を検索します。
///
public static string AutoScroll => ResourceManager.GetString("AutoScroll", resourceCulture) ?? string.Empty;
///
- /// "N" ɗގĂ郍[JCYꂽ܂B
+ /// "自動起動" に類似しているローカライズされた文字列を検索します。
///
public static string AutoStart => ResourceManager.GetString("AutoStart", resourceCulture) ?? string.Empty;
///
- /// "PCNɎN" ɗގĂ郍[JCYꂽ܂B
+ /// "PC起動時に自動起動" に類似しているローカライズされた文字列を検索します。
///
public static string AutoStartWithPC => ResourceManager.GetString("AutoStartWithPC", resourceCulture) ?? string.Empty;
///
- /// "|Ώ" ɗގĂ郍[JCYꂽ܂B
+ /// "自動翻訳対象" に類似しているローカライズされた文字列を検索します。
///
public static string AutoTargets => ResourceManager.GetString("AutoTargets", resourceCulture) ?? string.Empty;
///
- /// "rh" ɗގĂ郍[JCYꂽ܂B
+ /// "ビルド日時" に類似しているローカライズされた文字列を検索します。
///
public static string BuildDate => ResourceManager.GetString("BuildDate", resourceCulture) ?? string.Empty;
///
- /// "LbVW[" ɗގĂ郍[JCYꂽ܂B
+ /// "キャッシュモジュール" に類似しているローカライズされた文字列を検索します。
///
public static string CacheModule => ResourceManager.GetString("CacheModule", resourceCulture) ?? string.Empty;
///
- /// "LbVݒ" ɗގĂ郍[JCYꂽ܂B
+ /// "キャッシュ設定" に類似しているローカライズされた文字列を検索します。
///
public static string CacheParam => ResourceManager.GetString("CacheParam", resourceCulture) ?? string.Empty;
///
- /// "LZ" ɗގĂ郍[JCYꂽ܂B
+ /// "キャンセル" に類似しているローカライズされた文字列を検索します。
///
public static string Cancel => ResourceManager.GetString("Cancel", resourceCulture) ?? string.Empty;
///
- /// "Lv`[EBhE" ɗގĂ郍[JCYꂽ܂B
+ /// "キャプチャーウィンドウ" に類似しているローカライズされた文字列を検索します。
///
public static string Capture => ResourceManager.GetString("Capture", resourceCulture) ?? string.Empty;
///
- /// "Vo[W̃`FbN" ɗގĂ郍[JCYꂽ܂B
+ /// "新しいバージョンのチェック" に類似しているローカライズされた文字列を検索します。
///
public static string CheckNewVersion => ResourceManager.GetString("CheckNewVersion", resourceCulture) ?? string.Empty;
///
- /// "XVe̊mF" ɗގĂ郍[JCYꂽ܂B
+ /// "更新内容の確認" に類似しているローカライズされた文字列を検索します。
///
public static string CheckUpdateNotes => ResourceManager.GetString("CheckUpdateNotes", resourceCulture) ?? string.Empty;
///
- /// "NA" ɗގĂ郍[JCYꂽ܂B
+ /// "クリア" に類似しているローカライズされた文字列を検索します。
///
public static string Clear => ResourceManager.GetString("Clear", resourceCulture) ?? string.Empty;
///
- /// "" ɗގĂ郍[JCYꂽ܂B
+ /// "閉じる" に類似しているローカライズされた文字列を検索します。
///
public static string Close => ResourceManager.GetString("Close", resourceCulture) ?? string.Empty;
///
- /// "mF" ɗގĂ郍[JCYꂽ܂B
+ /// "確認" に類似しているローカライズされた文字列を検索します。
///
public static string Confirm => ResourceManager.GetString("Confirm", resourceCulture) ?? string.Empty;
///
- /// "Rs[܂" ɗގĂ郍[JCYꂽ܂B
+ /// "コピーしました" に類似しているローカライズされた文字列を検索します。
///
public static string Copied => ResourceManager.GetString("Copied", resourceCulture) ?? string.Empty;
///
- /// "Rs[" ɗގĂ郍[JCYꂽ܂B
+ /// "情報をコピー" に類似しているローカライズされた文字列を検索します。
///
public static string Copy => ResourceManager.GetString("Copy", resourceCulture) ?? string.Empty;
///
- /// "ftHgݒ" ɗގĂ郍[JCYꂽ܂B
+ /// "デフォルト設定" に類似しているローカライズされた文字列を検索します。
///
public static string DefaultSetting => ResourceManager.GetString("DefaultSetting", resourceCulture) ?? string.Empty;
///
- /// "f^b`" ɗގĂ郍[JCYꂽ܂B
+ /// "デタッチ" に類似しているローカライズされた文字列を検索します。
///
public static string Detach => ResourceManager.GetString("Detach", resourceCulture) ?? string.Empty;
///
- /// "Zp" ɗގĂ郍[JCYꂽ܂B
+ /// "技術情報" に類似しているローカライズされた文字列を検索します。
///
public static string Develop => ResourceManager.GetString("Develop", resourceCulture) ?? string.Empty;
///
- /// "J" ɗގĂ郍[JCYꂽ܂B
+ /// "開発者" に類似しているローカライズされた文字列を検索します。
///
public static string DevelopedBy => ResourceManager.GetString("DevelopedBy", resourceCulture) ?? string.Empty;
///
- /// "ACR\" ɗގĂ郍[JCYꂽ܂B
+ /// "処理中アイコンを表示する" に類似しているローカライズされた文字列を検索します。
///
public static string DisplayBusy => ResourceManager.GetString("DisplayBusy", resourceCulture) ?? string.Empty;
///
- /// "\@" ɗގĂ郍[JCYꂽ܂B
+ /// "表示方法" に類似しているローカライズされた文字列を検索します。
///
public static string DisplayMethod => ResourceManager.GetString("DisplayMethod", resourceCulture) ?? string.Empty;
///
- /// "I" ɗގĂ郍[JCYꂽ܂B
+ /// "終了" に類似しているローカライズされた文字列を検索します。
///
public static string Exit => ResourceManager.GetString("Exit", resourceCulture) ?? string.Empty;
///
- /// "GNX|[g" ɗގĂ郍[JCYꂽ܂B
+ /// "エクスポート" に類似しているローカライズされた文字列を検索します。
///
public static string Export => ResourceManager.GetString("Export", resourceCulture) ?? string.Empty;
///
- /// "ÕGNX|[g" ɗގĂ郍[JCYꂽ܂B
+ /// "ログのエクスポート" に類似しているローカライズされた文字列を検索します。
///
public static string ExportLogs => ResourceManager.GetString("ExportLogs", resourceCulture) ?? string.Empty;
///
- /// "GNX|[gs" ɗގĂ郍[JCYꂽ܂B
+ /// "エクスポート失敗" に類似しているローカライズされた文字列を検索します。
///
public static string ExportLogsFailed => ResourceManager.GetString("ExportLogsFailed", resourceCulture) ?? string.Empty;
///
- /// "eLXgt@C" ɗގĂ郍[JCYꂽ܂B
+ /// "テキストファイル" に類似しているローカライズされた文字列を検索します。
///
public static string ExportLogsFilterText => ResourceManager.GetString("ExportLogsFilterText", resourceCulture) ?? string.Empty;
///
- /// "GNX|[g" ɗގĂ郍[JCYꂽ܂B
+ /// "エクスポート完了" に類似しているローカライズされた文字列を検索します。
///
public static string ExportLogsSuccess => ResourceManager.GetString("ExportLogsSuccess", resourceCulture) ?? string.Empty;
///
- /// "O`{0}`ɃGNX|[g܂B" ɗގĂ郍[JCYꂽ܂B
+ /// "ログを`{0}`にエクスポートしました。" に類似しているローカライズされた文字列を検索します。
///
public static string ExportLogsSuccessDetail => ResourceManager.GetString("ExportLogsSuccessDetail", resourceCulture) ?? string.Empty;
///
- /// "ݒ̓KpɎs܂B" ɗގĂ郍[JCYꂽ܂B
+ /// "設定の適用に失敗しました。" に類似しているローカライズされた文字列を検索します。
///
public static string FaildApplySettings => ResourceManager.GetString("FaildApplySettings", resourceCulture) ?? string.Empty;
///
- /// "OCRɎs܂" ɗގĂ郍[JCYꂽ܂B
+ /// "OCRに失敗しました" に類似しているローカライズされた文字列を検索します。
///
public static string FaildOcr => ResourceManager.GetString("FaildOcr", resourceCulture) ?? string.Empty;
///
- /// "EBhE̖ߍ݂Ɏs܂B" ɗގĂ郍[JCYꂽ܂B
+ /// "ウィンドウの埋め込みに失敗しました。" に類似しているローカライズされた文字列を検索します。
///
public static string FaildOverlay => ResourceManager.GetString("FaildOverlay", resourceCulture) ?? string.Empty;
///
- /// "|Ɏs܂" ɗގĂ郍[JCYꂽ܂B
+ /// "翻訳に失敗しました" に類似しているローカライズされた文字列を検索します。
///
public static string FaildTranslate => ResourceManager.GetString("FaildTranslate", resourceCulture) ?? string.Empty;
///
- /// "߂eLXg臒l" ɗގĂ郍[JCYꂽ܂B
+ /// "近いテキストの閾値" に類似しているローカライズされた文字列を検索します。
///
public static string FuzzyMatchThreshold => ResourceManager.GetString("FuzzyMatchThreshold", resourceCulture) ?? string.Empty;
///
- /// "Sʐݒ" ɗގĂ郍[JCYꂽ܂B
+ /// "全般設定" に類似しているローカライズされた文字列を検索します。
///
public static string GeneralSettings => ResourceManager.GetString("GeneralSettings", resourceCulture) ?? string.Empty;
///
- /// "Abvf[g܂: {0}" ɗގĂ郍[JCYꂽ܂B
+ /// "アップデートがあります: {0}" に類似しているローカライズされた文字列を検索します。
///
public static string HasUpdate => ResourceManager.GetString("HasUpdate", resourceCulture) ?? string.Empty;
///
- /// "ĂԂ" ɗގĂ郍[JCYꂽ܂B
+ /// "押している間だけ" に類似しているローカライズされた文字列を検索します。
///
public static string Hold => ResourceManager.GetString("Hold", resourceCulture) ?? string.Empty;
///
- /// "LbV" ɗގĂ郍[JCYꂽ܂B
+ /// "メモリ内キャッシュ" に類似しているローカライズされた文字列を検索します。
///
public static string InMemoryCache => ResourceManager.GetString("InMemoryCache", resourceCulture) ?? string.Empty;
///
- /// "Vo[W: {0} ̃CXg[" ɗގĂ郍[JCYꂽ܂B
+ /// "インストール" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string Install => ResourceManager.GetString("Install", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "インストール済み" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string Installed => ResourceManager.GetString("Installed", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "インストール済み: {0}" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string InstalledVersion => ResourceManager.GetString("InstalledVersion", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "インストール済みバージョン" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string InstalledVersionLabel => ResourceManager.GetString("InstalledVersionLabel", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "新しいバージョン: {0} のインストール" に類似しているローカライズされた文字列を検索します。
///
public static string InstallNewVersion => ResourceManager.GetString("InstallNewVersion", resourceCulture) ?? string.Empty;
///
- /// "{0}: ݒ茟G[" ɗގĂ郍[JCYꂽ܂B
+ /// "{0}: 設定検証エラー" に類似しているローカライズされた文字列を検索します。
///
public static string InvalidSettings => ResourceManager.GetString("InvalidSettings", resourceCulture) ?? string.Empty;
///
- /// ":tired-face: **̂܂sĂ삵Ȃ\ł** &a..." ɗގĂ郍[JCYꂽ܂B
+ /// ":tired-face: **そのまま実行しても動作しない可能性が高いです** **..." に類似しているローカライズされた文字列を検索します。
///
public static string InvalidSettingsContent => ResourceManager.GetString("InvalidSettingsContent", resourceCulture) ?? string.Empty;
///
- /// "x|ΏۂɑIvZXNƂɎIɖ|" ɗގĂ郍[JCYꂽ܂B
+ /// "一度翻訳対象に選択したプロセスが起動したときに自動的に翻訳する" に類似しているローカライズされた文字列を検索します。
///
public static string IsEnableAutoTarget => ResourceManager.GetString("IsEnableAutoTarget", resourceCulture) ?? string.Empty;
///
- /// "I[o[C\Lv`[\ɂ" ɗގĂ郍[JCYꂽ܂B
+ /// "オーバーレイ表示をキャプチャー可能にする" に類似しているローカライズされた文字列を検索します。
///
public static string IsEnableCaptureOverlay => ResourceManager.GetString("IsEnableCaptureOverlay", resourceCulture) ?? string.Empty;
///
- /// "ŐVo[WpłB" ɗގĂ郍[JCYꂽ܂B
+ /// "最新バージョンをご利用中です。" に類似しているローカライズされた文字列を検索します。
///
public static string IsLatest => ResourceManager.GetString("IsLatest", resourceCulture) ?? string.Empty;
///
- /// "I[oCLɂ^C~Ôݖ|" ɗގĂ郍[JCYꂽ܂B
+ /// "オーバレイを有効にしたタイミングのみ翻訳する" に類似しているローカライズされた文字列を検索します。
///
public static string IsOneShotMode => ResourceManager.GetString("IsOneShotMode", resourceCulture) ?? string.Empty;
///
- /// "}EX|C^[ʒũeLXĝ݃I[oC|\" ɗގĂ郍[JCYꂽ܂B
+ /// "マウスポインター位置のテキストのみオーバレイ翻訳を表示する" に類似しているローカライズされた文字列を検索します。
///
public static string IsOverlayPointSwap => ResourceManager.GetString("IsOverlayPointSwap", resourceCulture) ?? string.Empty;
///
- /// "ݒ" ɗގĂ郍[JCYꂽ܂B
+ /// "言語設定" に類似しているローカライズされた文字列を検索します。
///
public static string Language => ResourceManager.GetString("Language", resourceCulture) ?? string.Empty;
///
- /// "CZX" ɗގĂ郍[JCYꂽ܂B
+ /// "最新バージョン" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string LatestVersion => ResourceManager.GetString("LatestVersion", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "ライセンス" に類似しているローカライズされた文字列を検索します。
///
public static string License => ResourceManager.GetString("License", resourceCulture) ?? string.Empty;
///
- /// "[Jt@CLbV" ɗގĂ郍[JCYꂽ܂B
+ /// "ライセンス情報" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string LicenseUrl => ResourceManager.GetString("LicenseUrl", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "ローカルファイルキャッシュ" に類似しているローカライズされた文字列を検索します。
///
public static string LocalCache => ResourceManager.GetString("LocalCache", resourceCulture) ?? string.Empty;
///
- /// "O" ɗގĂ郍[JCYꂽ܂B
+ /// "ログ" に類似しているローカライズされた文字列を検索します。
///
public static string Log => ResourceManager.GetString("Log", resourceCulture) ?? string.Empty;
///
- /// "̑" ɗގĂ郍[JCYꂽ܂B
+ /// "その他" に類似しているローカライズされた文字列を検索します。
///
public static string Misc => ResourceManager.GetString("Misc", resourceCulture) ?? string.Empty;
///
- /// "łWindowTranslatorNł" ɗގĂ郍[JCYꂽ܂B
+ /// "すでにWindowTranslatorが起動中です" に類似しているローカライズされた文字列を検索します。
///
public static string MutexError => ResourceManager.GetString("MutexError", resourceCulture) ?? string.Empty;
///
- /// "Vo[W: {0} [X܂" ɗގĂ郍[JCYꂽ܂B
+ /// "新しいバージョン: {0} がリリースされました" に類似しているローカライズされた文字列を検索します。
///
public static string NewVersionAvailable => ResourceManager.GetString("NewVersionAvailable", resourceCulture) ?? string.Empty;
///
- /// "LbVȂ" ɗގĂ郍[JCYꂽ܂B
+ /// "キャッシュしない" に類似しているローカライズされた文字列を検索します。
///
public static string NoCache => ResourceManager.GetString("NoCache", resourceCulture) ?? string.Empty;
///
- /// "|Ȃ" ɗގĂ郍[JCYꂽ܂B
+ /// "翻訳しない" に類似しているローカライズされた文字列を検索します。
///
public static string NoTranslateModule => ResourceManager.GetString("NoTranslateModule", resourceCulture) ?? string.Empty;
///
- /// "{0}OCR@\g܂BΏۂ̌@\CXg[Ă" ɗގĂ郍[JCYꂽ܂B
+ /// "NuGetからのプラグイン一覧の取得に失敗しました。ネットワーク接続を確認してください。" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string NuGetSearchFailed => ResourceManager.GetString("NuGetSearchFailed", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "{0}のOCR機能が使えません。対象の言語機能をインストールしてください" に類似しているローカライズされた文字列を検索します。
///
public static string OcrLanguageNotAvailable => ResourceManager.GetString("OcrLanguageNotAvailable", resourceCulture) ?? string.Empty;
///
- /// "FW[" ɗގĂ郍[JCYꂽ܂B
+ /// "認識モジュール" に類似しているローカライズされた文字列を検索します。
///
public static string OcrModule => ResourceManager.GetString("OcrModule", resourceCulture) ?? string.Empty;
///
- /// "OK" ɗގĂ郍[JCYꂽ܂B
+ /// "OK" に類似しているローカライズされた文字列を検索します。
///
public static string OK => ResourceManager.GetString("OK", resourceCulture) ?? string.Empty;
///
- /// "ڍ̊mF" ɗގĂ郍[JCYꂽ܂B
+ /// "詳細情報の確認" に類似しているローカライズされた文字列を検索します。
///
public static string OpenChangelogCommand => ResourceManager.GetString("OpenChangelogCommand", resourceCulture) ?? string.Empty;
///
- /// "T[hp[eB[CZX̊mF" ɗގĂ郍[JCYꂽ܂B
+ /// "サードパーティーライセンスの確認" に類似しているローカライズされた文字列を検索します。
///
public static string OpenThirdPartyLicensesCommand => ResourceManager.GetString("OpenThirdPartyLicensesCommand", resourceCulture) ?? string.Empty;
///
- /// "I[oC" ɗގĂ郍[JCYꂽ܂B
+ /// "オーバレイ" に類似しているローカライズされた文字列を検索します。
///
public static string Overlay => ResourceManager.GetString("Overlay", resourceCulture) ?? string.Empty;
///
- /// "I[o[Cwi̕sx" ɗގĂ郍[JCYꂽ܂B
+ /// "オーバーレイ背景の不透明度" に類似しているローカライズされた文字列を検索します。
///
public static string OverlayOpacity => ResourceManager.GetString("OverlayOpacity", resourceCulture) ?? string.Empty;
///
- /// "I[o[Cւ" ɗގĂ郍[JCYꂽ܂B
+ /// "オーバーレイ切り替え" に類似しているローカライズされた文字列を検索します。
///
public static string OverlayShortcut => ResourceManager.GetString("OverlayShortcut", resourceCulture) ?? string.Empty;
///
- /// "I[o[C\̐ւ" ɗގĂ郍[JCYꂽ܂B
+ /// "オーバーレイ表示の切り替え" に類似しているローカライズされた文字列を検索します。
///
public static string OverlaySwitch => ResourceManager.GetString("OverlaySwitch", resourceCulture) ?? string.Empty;
///
- /// "vOCݒ" ɗގĂ郍[JCYꂽ܂B
+ /// "プラグイン設定" に類似しているローカライズされた文字列を検索します。
///
public static string Plugin => ResourceManager.GetString("Plugin", resourceCulture) ?? string.Empty;
///
- /// "Jy[W" ɗގĂ郍[JCYꂽ܂B
+ /// "インストール失敗" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string PluginInstallFailed => ResourceManager.GetString("PluginInstallFailed", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "インストール完了" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string PluginInstallSuccess => ResourceManager.GetString("PluginInstallSuccess", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "プラグインストア" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string PluginStore => ResourceManager.GetString("PluginStore", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "プロジェクトページ" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string ProjectUrl => ResourceManager.GetString("ProjectUrl", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "公開ページ" に類似しているローカライズされた文字列を検索します。
///
public static string PublishPage => ResourceManager.GetString("PublishPage", resourceCulture) ?? string.Empty;
///
- /// "{0}Nɓo^܂B" ɗގĂ郍[JCYꂽ܂B
+ /// "{0}を自動起動に登録しました。" に類似しているローカライズされた文字列を検索します。
///
public static string RegisterAutoStart => ResourceManager.GetString("RegisterAutoStart", resourceCulture) ?? string.Empty;
///
- /// "" ɗގĂ郍[JCYꂽ܂B
+ /// "プラグインの変更を適用するには、WindowTranslatorを再起動してください。" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string RestartRequired => ResourceManager.GetString("RestartRequired", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "後で" に類似しているローカライズされた文字列を検索します。
///
public static string ReviewLater => ResourceManager.GetString("ReviewLater", resourceCulture) ?? string.Empty;
///
- /// "xƕ\Ȃ" ɗގĂ郍[JCYꂽ܂B
+ /// "二度と表示しない" に類似しているローカライズされた文字列を検索します。
///
public static string ReviewNeverShowAgain => ResourceManager.GetString("ReviewNeverShowAgain", resourceCulture) ?? string.Empty;
///
- /// "r[̂肢" ɗގĂ郍[JCYꂽ܂B
+ /// "レビューのお願い" に類似しているローカライズされた文字列を検索します。
///
public static string ReviewRequest => ResourceManager.GetString("ReviewRequest", resourceCulture) ?? string.Empty;
///
- /// "WindowTranslatorp肪Ƃ܂BMicrosoft Store..." ɗގĂ郍[JCYꂽ܂B
+ /// "WindowTranslatorをご利用いただきありがとうございます。Microsoft Store..." に類似しているローカライズされた文字列を検索します。
///
public static string ReviewRequestMessage => ResourceManager.GetString("ReviewRequestMessage", resourceCulture) ?? string.Empty;
///
- /// "̂܂s" ɗގĂ郍[JCYꂽ܂B
+ /// "そのまま実行" に類似しているローカライズされた文字列を検索します。
///
public static string RunAsIs => ResourceManager.GetString("RunAsIs", resourceCulture) ?? string.Empty;
///
- /// "|Ɩ|挾ꂪłBقȂ錾w肵ĂB" ɗގĂ郍[JCYꂽ܂B
+ /// "翻訳元言語と翻訳先言語が同一です。異なる言語を指定してください。" に類似しているローカライズされた文字列を検索します。
///
public static string SameSourceTargetLanguage => ResourceManager.GetString("SameSourceTargetLanguage", resourceCulture) ?? string.Empty;
///
- /// "ۑĕ" ɗގĂ郍[JCYꂽ܂B
+ /// "保存して閉じる" に類似しているローカライズされた文字列を検索します。
///
public static string SaveAndClose => ResourceManager.GetString("SaveAndClose", resourceCulture) ?? string.Empty;
///
- /// "WindowTranslatorȊÕEBhEIĂ" ɗގĂ郍[JCYꂽ܂B
+ /// "WindowTranslator以外のウィンドウを選択してください" に類似しているローカライズされた文字列を検索します。
///
public static string SelectOtherWindow => ResourceManager.GetString("SelectOtherWindow", resourceCulture) ?? string.Empty;
///
- /// "G[|[gVXeɑM܂Bȉ̏M܂B ..." ɗގĂ郍[JCYꂽ܂B
+ /// "エラー情報をレポートシステムに送信します。以下の情報が送信されます。 * アプリ情報..." に類似しているローカライズされた文字列を検索します。
///
public static string SendReportToolTip => ResourceManager.GetString("SendReportToolTip", resourceCulture) ?? string.Empty;
///
- /// "𑗐M" ɗގĂ郍[JCYꂽ܂B
+ /// "情報を送信" に類似しているローカライズされた文字列を検索します。
///
public static string SendRerpot => ResourceManager.GetString("SendRerpot", resourceCulture) ?? string.Empty;
///
- /// "M" ɗގĂ郍[JCYꂽ܂B
+ /// "送信完了" に類似しているローカライズされた文字列を検索します。
///
public static string Sent => ResourceManager.GetString("Sent", resourceCulture) ?? string.Empty;
///
- /// ":tired-face: **̂܂ܕۑĂ삵܂B** 
..." ɗގĂ郍[JCYꂽ܂B
+ /// ":tired-face: **このまま保存しても動作しません。** ***&..." に類似しているローカライズされた文字列を検索します。
///
public static string SettingInvalidContent => ResourceManager.GetString("SettingInvalidContent", resourceCulture) ?? string.Empty;
///
- /// "ݒ" ɗގĂ郍[JCYꂽ܂B
+ /// "設定" に類似しているローカライズされた文字列を検索します。
///
public static string Settings => ResourceManager.GetString("Settings", resourceCulture) ?? string.Empty;
///
- /// "ݒ茟G[" ɗގĂ郍[JCYꂽ܂B
+ /// "設定検証エラー" に類似しているローカライズされた文字列を検索します。
///
public static string SettingsInvalid => ResourceManager.GetString("SettingsInvalid", resourceCulture) ?? string.Empty;
///
- /// "S̐ݒ" ɗގĂ郍[JCYꂽ܂B
+ /// "全体設定" に類似しているローカライズされた文字列を検索します。
///
public static string SettingsViewModel => ResourceManager.GetString("SettingsViewModel", resourceCulture) ?? string.Empty;
///
- /// "V[gJbg" ɗގĂ郍[JCYꂽ܂B
+ /// "ショートカット" に類似しているローカライズされた文字列を検索します。
///
public static string Shortcut => ResourceManager.GetString("Shortcut", resourceCulture) ?? string.Empty;
///
- /// "|(F)" ɗގĂ郍[JCYꂽ܂B
+ /// "翻訳元(認識)言語" に類似しているローカライズされた文字列を検索します。
///
public static string Source => ResourceManager.GetString("Source", resourceCulture) ?? string.Empty;
///
- /// "SteamŃQ[Mtg" ɗގĂ郍[JCYꂽ܂B
+ /// "Steamでゲームをギフト" に類似しているローカライズされた文字列を検索します。
///
public static string SteamWishlist => ResourceManager.GetString("SteamWishlist", resourceCulture) ?? string.Empty;
///
- /// "M" ɗގĂ郍[JCYꂽ܂B
+ /// "送信" に類似しているローカライズされた文字列を検索します。
///
public static string Submit => ResourceManager.GetString("Submit", resourceCulture) ?? string.Empty;
///
- /// "|(\)" ɗގĂ郍[JCYꂽ܂B
+ /// "翻訳先(表示)言語" に類似しているローカライズされた文字列を検索します。
///
public static string Target => ResourceManager.GetString("Target", resourceCulture) ?? string.Empty;
///
- /// "|ΏۃvZX" ɗގĂ郍[JCYꂽ܂B
+ /// "翻訳対象プロセス" に類似しているローカライズされた文字列を検索します。
///
public static string TargetProcesses => ResourceManager.GetString("TargetProcesses", resourceCulture) ?? string.Empty;
///
- /// "ΏۂƂ̐ݒ" ɗގĂ郍[JCYꂽ܂B
+ /// "対象ごとの設定" に類似しているローカライズされた文字列を検索します。
///
public static string TargetSpecificSettings => ResourceManager.GetString("TargetSpecificSettings", resourceCulture) ?? string.Empty;
///
- /// "Av" ɗގĂ郍[JCYꂽ܂B
+ /// "アプリ名" に類似しているローカライズされた文字列を検索します。
///
public static string Title => ResourceManager.GetString("Title", resourceCulture) ?? string.Empty;
///
- /// "ON/OFFւ" ɗގĂ郍[JCYꂽ܂B
+ /// "押してON/OFFを切り替える" に類似しているローカライズされた文字列を検索します。
///
public static string Toggle => ResourceManager.GetString("Toggle", resourceCulture) ?? string.Empty;
///
- /// "ݒ" ɗގĂ郍[JCYꂽ܂B
+ /// "言語設定" に類似しているローカライズされた文字列を検索します。
///
public static string TranslateLanguage => ResourceManager.GetString("TranslateLanguage", resourceCulture) ?? string.Empty;
///
- /// "|W[" ɗގĂ郍[JCYꂽ܂B
+ /// "翻訳モジュール" に類似しているローカライズされた文字列を検索します。
///
public static string TranslateModule => ResourceManager.GetString("TranslateModule", resourceCulture) ?? string.Empty;
///
- /// "sȃG[܂" ɗގĂ郍[JCYꂽ܂B
+ /// "不明なエラーが発生しました" に類似しているローカライズされた文字列を検索します。
///
public static string UnhundledErrorMessage => ResourceManager.GetString("UnhundledErrorMessage", resourceCulture) ?? string.Empty;
///
- /// "IEBhEu{0}v̓vZXłȂ߁ALv`[o܂B ..." ɗގĂ郍[JCYꂽ܂B
+ /// "アンインストール" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string Uninstall => ResourceManager.GetString("Uninstall", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "{0} をアンインストールしますか?次回起動時に完全に削除されます。" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string UninstallConfirm => ResourceManager.GetString("UninstallConfirm", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "選択したウィンドウ「{0}」はプロセスを特定できないため、キャプチャー出来ません。 ..." に類似しているローカライズされた文字列を検索します。
///
public static string UnknownWindow => ResourceManager.GetString("UnknownWindow", resourceCulture) ?? string.Empty;
///
- /// "{0}̎N܂B" ɗގĂ郍[JCYꂽ܂B
+ /// "{0}の自動起動を解除しました。" に類似しているローカライズされた文字列を検索します。
///
public static string UnregisterAutoStart => ResourceManager.GetString("UnregisterAutoStart", resourceCulture) ?? string.Empty;
///
- /// "ŐVo[WɍXV" ɗގĂ郍[JCYꂽ܂B
+ /// "更新" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string Update => ResourceManager.GetString("Update", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "更新あり" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string UpdateAvailable => ResourceManager.GetString("UpdateAvailable", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "インストール済み: {0} → 最新: {1}" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string UpdateAvailableVersion => ResourceManager.GetString("UpdateAvailableVersion", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "最新バージョンに更新" に類似しているローカライズされた文字列を検索します。
///
public static string UpdateCommand => ResourceManager.GetString("UpdateCommand", resourceCulture) ?? string.Empty;
///
- /// "XV" ɗގĂ郍[JCYꂽ܂B
+ /// "更新情報" に類似しているローカライズされた文字列を検索します。
///
public static string UpdateInfo => ResourceManager.GetString("UpdateInfo", resourceCulture) ?? string.Empty;
///
- /// "o[W" ɗގĂ郍[JCYꂽ܂B
+ /// "バージョン" に類似しているローカライズされた文字列を検索します。
///
public static string Version => ResourceManager.GetString("Version", resourceCulture) ?? string.Empty;
///
- /// "|ʕ\[h" ɗގĂ郍[JCYꂽ܂B
+ /// "翻訳結果表示モード" に類似しているローカライズされた文字列を検索します。
///
public static string ViewMode => ResourceManager.GetString("ViewMode", resourceCulture) ?? string.Empty;
///
- /// "WindowsWF" ɗގĂ郍[JCYꂽ܂B
+ /// "Windows標準文字認識" に類似しているローカライズされた文字列を検索します。
///
public static string WindowsMediaOcr => ResourceManager.GetString("WindowsMediaOcr", resourceCulture) ?? string.Empty;
///
- /// "r[" ɗގĂ郍[JCYꂽ܂B
+ /// "レビューする" に類似しているローカライズされた文字列を検索します。
///
public static string WriteReview => ResourceManager.GetString("WriteReview", resourceCulture) ?? string.Empty;
}
diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx
index 63af0458..dba7cf5a 100644
--- a/WindowTranslator/Properties/Resources.ar.resx
+++ b/WindowTranslator/Properties/Resources.ar.resx
@@ -447,4 +447,55 @@
+
+ متجر المكونات الإضافية
+
+
+ تثبيت
+
+
+ مثبت
+
+
+ تحديث
+
+
+ إلغاء التثبيت
+
+
+ هل أنت متأكد من رغبتك في إلغاء تثبيت {0}؟ سيتم حذفه بالكامل عند التشغيل التالي.
+
+
+ يتوفر تحديث
+
+
+ مثبت: {0} → الأحدث: {1}
+
+
+ مثبت: {0}
+
+
+ الإصدار المثبت
+
+
+ أحدث إصدار
+
+
+ فشل استرداد قائمة المكونات الإضافية من NuGet. يرجى التحقق من اتصالك بالشبكة.
+
+
+ اكتمل التثبيت
+
+
+ فشل التثبيت
+
+
+ يرجى إعادة تشغيل WindowTranslator لتطبيق تغييرات المكون الإضافي.
+
+
+ صفحة المشروع
+
+
+ معلومات الترخيص
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx
index 83b05eb1..69ba7493 100644
--- a/WindowTranslator/Properties/Resources.cs.resx
+++ b/WindowTranslator/Properties/Resources.cs.resx
@@ -343,4 +343,55 @@ Monitory nejsou podporovány.
+
+ Obchod s pluginy
+
+
+ Nainstalovat
+
+
+ Nainstalováno
+
+
+ Aktualizovat
+
+
+ Odinstalovat
+
+
+ Opravdu chcete odinstalovat {0}? Bude zcela odstraněn při příštím spuštění.
+
+
+ Dostupná aktualizace
+
+
+ Nainstalováno: {0} → Nejnovější: {1}
+
+
+ Nainstalováno: {0}
+
+
+ Nainstalovaná verze
+
+
+ Nejnovější verze
+
+
+ Nepodařilo se načíst seznam pluginů z NuGet. Zkontrolujte připojení k síti.
+
+
+ Instalace dokončena
+
+
+ Instalace se nezdařila
+
+
+ Restartujte WindowTranslator, aby se změny pluginu projevily.
+
+
+ Stránka projektu
+
+
+ Informace o licenci
+
diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx
index 00177fe2..0c653608 100644
--- a/WindowTranslator/Properties/Resources.de.resx
+++ b/WindowTranslator/Properties/Resources.de.resx
@@ -456,4 +456,55 @@ Monitore werden nicht unterstützt.
+
+ Plugin-Store
+
+
+ Installieren
+
+
+ Installiert
+
+
+ Aktualisieren
+
+
+ Deinstallieren
+
+
+ Möchten Sie {0} wirklich deinstallieren? Es wird beim nächsten Start vollständig entfernt.
+
+
+ Update verfügbar
+
+
+ Installiert: {0} → Aktuell: {1}
+
+
+ Installiert: {0}
+
+
+ Installierte Version
+
+
+ Neueste Version
+
+
+ Fehler beim Abrufen der Plugin-Liste von NuGet. Bitte überprüfen Sie Ihre Netzwerkverbindung.
+
+
+ Installation abgeschlossen
+
+
+ Installation fehlgeschlagen
+
+
+ Bitte starten Sie WindowTranslator neu, um Plugin-Änderungen anzuwenden.
+
+
+ Projektseite
+
+
+ Lizenzinformationen
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx
index 74c0acfa..7f9058d2 100644
--- a/WindowTranslator/Properties/Resources.en.resx
+++ b/WindowTranslator/Properties/Resources.en.resx
@@ -456,4 +456,55 @@ Monitors are not supported.
+
+ Plugin Store
+
+
+ Install
+
+
+ Installed
+
+
+ Update
+
+
+ Uninstall
+
+
+ Are you sure you want to uninstall {0}? It will be fully removed on next startup.
+
+
+ Update available
+
+
+ Installed: {0} → Latest: {1}
+
+
+ Installed: {0}
+
+
+ Installed version
+
+
+ Latest version
+
+
+ Failed to retrieve plugin list from NuGet. Please check your network connection.
+
+
+ Installation complete
+
+
+ Installation failed
+
+
+ Please restart WindowTranslator to apply plugin changes.
+
+
+ Project page
+
+
+ License information
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx
index d22a3c3a..7521e7a6 100644
--- a/WindowTranslator/Properties/Resources.es.resx
+++ b/WindowTranslator/Properties/Resources.es.resx
@@ -447,4 +447,55 @@
+
+ Tienda de plugins
+
+
+ Instalar
+
+
+ Instalado
+
+
+ Actualizar
+
+
+ Desinstalar
+
+
+ ¿Está seguro de que desea desinstalar {0}? Se eliminará completamente en el próximo inicio.
+
+
+ Actualización disponible
+
+
+ Instalado: {0} → Último: {1}
+
+
+ Instalado: {0}
+
+
+ Versión instalada
+
+
+ Última versión
+
+
+ Error al recuperar la lista de plugins de NuGet. Compruebe su conexión de red.
+
+
+ Instalación completada
+
+
+ Error de instalación
+
+
+ Reinicie WindowTranslator para aplicar los cambios del plugin.
+
+
+ Página del proyecto
+
+
+ Información de licencia
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx
index ce535156..0b3838f9 100644
--- a/WindowTranslator/Properties/Resources.fa.resx
+++ b/WindowTranslator/Properties/Resources.fa.resx
@@ -447,4 +447,55 @@
+
+ فروشگاه افزونه
+
+
+ نصب
+
+
+ نصب شده
+
+
+ بهروزرسانی
+
+
+ حذف
+
+
+ آیا مطمئن هستید که میخواهید {0} را حذف کنید؟ در راهاندازی بعدی به طور کامل حذف خواهد شد.
+
+
+ بهروزرسانی موجود است
+
+
+ نصب شده: {0} → جدیدترین: {1}
+
+
+ نصب شده: {0}
+
+
+ نسخه نصب شده
+
+
+ جدیدترین نسخه
+
+
+ دریافت لیست افزونه از NuGet ناموفق بود. لطفاً اتصال شبکه خود را بررسی کنید.
+
+
+ نصب کامل شد
+
+
+ نصب ناموفق بود
+
+
+ لطفاً WindowTranslator را مجدداً راهاندازی کنید تا تغییرات افزونه اعمال شود.
+
+
+ صفحه پروژه
+
+
+ اطلاعات مجوز
+
diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx
index 657dde0d..7ac38b90 100644
--- a/WindowTranslator/Properties/Resources.fil.resx
+++ b/WindowTranslator/Properties/Resources.fil.resx
@@ -456,4 +456,55 @@ Ang monitor ay hindi suportado.
+
+ Plugin Store
+
+
+ I-install
+
+
+ Naka-install
+
+
+ I-update
+
+
+ I-uninstall
+
+
+ Sigurado ka bang gusto mong i-uninstall ang {0}? Ito ay ganap na matatanggal sa susunod na pagsisimula.
+
+
+ Available ang update
+
+
+ Naka-install: {0} → Pinakabago: {1}
+
+
+ Naka-install: {0}
+
+
+ Naka-install na bersyon
+
+
+ Pinakabagong bersyon
+
+
+ Nabigo sa pagkuha ng listahan ng plugin mula sa NuGet. Pakisuri ang iyong koneksyon sa network.
+
+
+ Natapos ang pag-install
+
+
+ Nabigo ang pag-install
+
+
+ Mangyaring i-restart ang WindowTranslator upang mailapat ang mga pagbabago sa plugin.
+
+
+ Pahina ng proyekto
+
+
+ Impormasyon ng lisensya
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx
index f4c7038c..258d72e0 100644
--- a/WindowTranslator/Properties/Resources.fr.resx
+++ b/WindowTranslator/Properties/Resources.fr.resx
@@ -447,4 +447,55 @@
+
+ Boutique de plugins
+
+
+ Installer
+
+
+ Installé
+
+
+ Mettre à jour
+
+
+ Désinstaller
+
+
+ Voulez-vous vraiment désinstaller {0} ? Il sera complètement supprimé au prochain démarrage.
+
+
+ Mise à jour disponible
+
+
+ Installé : {0} → Dernier : {1}
+
+
+ Installé : {0}
+
+
+ Version installée
+
+
+ Dernière version
+
+
+ Échec de la récupération de la liste des plugins depuis NuGet. Vérifiez votre connexion réseau.
+
+
+ Installation terminée
+
+
+ Échec de l'installation
+
+
+ Veuillez redémarrer WindowTranslator pour appliquer les modifications de plugin.
+
+
+ Page du projet
+
+
+ Informations de licence
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index e93cc645..ba7b6383 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -455,4 +455,55 @@
+
+ Plugin Store
+
+
+ Install
+
+
+ Installed
+
+
+ Update
+
+
+ Uninstall
+
+
+ Are you sure you want to uninstall {0}? It will be fully removed on next startup.
+
+
+ Update available
+
+
+ Installed: {0} → Latest: {1}
+
+
+ Installed: {0}
+
+
+ Installed version
+
+
+ Latest version
+
+
+ Failed to retrieve plugin list from NuGet. Please check your network connection.
+
+
+ Installation complete
+
+
+ Installation failed
+
+
+ Please restart WindowTranslator to apply plugin changes.
+
+
+ Project page
+
+
+ License information
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx
index 494a9598..ca130e23 100644
--- a/WindowTranslator/Properties/Resources.id.resx
+++ b/WindowTranslator/Properties/Resources.id.resx
@@ -455,4 +455,55 @@ Monitor tidak didukung.
+
+ Toko Plugin
+
+
+ Pasang
+
+
+ Terpasang
+
+
+ Perbarui
+
+
+ Hapus
+
+
+ Apakah Anda yakin ingin menghapus {0}? Ini akan dihapus sepenuhnya saat startup berikutnya.
+
+
+ Pembaruan tersedia
+
+
+ Terpasang: {0} → Terbaru: {1}
+
+
+ Terpasang: {0}
+
+
+ Versi terpasang
+
+
+ Versi terbaru
+
+
+ Gagal mengambil daftar plugin dari NuGet. Silakan periksa koneksi jaringan Anda.
+
+
+ Instalasi selesai
+
+
+ Instalasi gagal
+
+
+ Silakan restart WindowTranslator untuk menerapkan perubahan plugin.
+
+
+ Halaman proyek
+
+
+ Informasi lisensi
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx
index 186b4ee9..a285463a 100644
--- a/WindowTranslator/Properties/Resources.ko.resx
+++ b/WindowTranslator/Properties/Resources.ko.resx
@@ -456,4 +456,55 @@
+
+ 플러그인 스토어
+
+
+ 설치
+
+
+ 설치됨
+
+
+ 업데이트
+
+
+ 제거
+
+
+ {0}을(를) 제거하시겠습니까? 다음 시작 시 완전히 제거됩니다.
+
+
+ 업데이트 있음
+
+
+ 설치됨: {0} → 최신: {1}
+
+
+ 설치됨: {0}
+
+
+ 설치된 버전
+
+
+ 최신 버전
+
+
+ NuGet에서 플러그인 목록을 가져오지 못했습니다. 네트워크 연결을 확인하세요.
+
+
+ 설치 완료
+
+
+ 설치 실패
+
+
+ 플러그인 변경 사항을 적용하려면 WindowTranslator를 다시 시작하세요.
+
+
+ 프로젝트 페이지
+
+
+ 라이선스 정보
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx
index 6f2dbe69..34fc87e5 100644
--- a/WindowTranslator/Properties/Resources.ms.resx
+++ b/WindowTranslator/Properties/Resources.ms.resx
@@ -455,4 +455,55 @@ Monitor tidak disokong.
+
+ Kedai Plugin
+
+
+ Pasang
+
+
+ Dipasang
+
+
+ Kemaskini
+
+
+ Nyahpasang
+
+
+ Adakah anda pasti mahu menyahpasang {0}? Ia akan dibuang sepenuhnya semasa permulaan seterusnya.
+
+
+ Kemaskini tersedia
+
+
+ Dipasang: {0} → Terkini: {1}
+
+
+ Dipasang: {0}
+
+
+ Versi yang dipasang
+
+
+ Versi terkini
+
+
+ Gagal mendapatkan senarai plugin dari NuGet. Sila semak sambungan rangkaian anda.
+
+
+ Pemasangan selesai
+
+
+ Pemasangan gagal
+
+
+ Sila mulakan semula WindowTranslator untuk menerapkan perubahan plugin.
+
+
+ Halaman projek
+
+
+ Maklumat lesen
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx
index 93400dac..45d1c620 100644
--- a/WindowTranslator/Properties/Resources.pl.resx
+++ b/WindowTranslator/Properties/Resources.pl.resx
@@ -456,4 +456,55 @@ Monitory nie są obsługiwane.
+
+ Sklep wtyczek
+
+
+ Zainstaluj
+
+
+ Zainstalowano
+
+
+ Aktualizuj
+
+
+ Odinstaluj
+
+
+ Czy na pewno chcesz odinstalować {0}? Zostanie całkowicie usunięty przy następnym uruchomieniu.
+
+
+ Dostępna aktualizacja
+
+
+ Zainstalowano: {0} → Najnowszy: {1}
+
+
+ Zainstalowano: {0}
+
+
+ Zainstalowana wersja
+
+
+ Najnowsza wersja
+
+
+ Nie udało się pobrać listy wtyczek z NuGet. Sprawdź połączenie sieciowe.
+
+
+ Instalacja zakończona
+
+
+ Instalacja nie powiodła się
+
+
+ Uruchom ponownie WindowTranslator, aby zastosować zmiany wtyczki.
+
+
+ Strona projektu
+
+
+ Informacje o licencji
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx
index 9ae483a0..abb41766 100644
--- a/WindowTranslator/Properties/Resources.pt-BR.resx
+++ b/WindowTranslator/Properties/Resources.pt-BR.resx
@@ -455,4 +455,55 @@ Monitor tidak didukung.
+
+ Loja de Plugins
+
+
+ Instalar
+
+
+ Instalado
+
+
+ Atualizar
+
+
+ Desinstalar
+
+
+ Tem certeza que deseja desinstalar {0}? Ele será completamente removido na próxima inicialização.
+
+
+ Atualização disponível
+
+
+ Instalado: {0} → Mais recente: {1}
+
+
+ Instalado: {0}
+
+
+ Versão instalada
+
+
+ Versão mais recente
+
+
+ Falha ao recuperar lista de plugins do NuGet. Verifique sua conexão de rede.
+
+
+ Instalação concluída
+
+
+ Falha na instalação
+
+
+ Reinicie o WindowTranslator para aplicar as alterações de plugin.
+
+
+ Página do projeto
+
+
+ Informações de licença
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx
index 5e6bfd5f..91b432e6 100644
--- a/WindowTranslator/Properties/Resources.resx
+++ b/WindowTranslator/Properties/Resources.resx
@@ -456,4 +456,55 @@
+
+ プラグインストア
+
+
+ インストール
+
+
+ インストール済み
+
+
+ 更新
+
+
+ アンインストール
+
+
+ {0} をアンインストールしますか?次回起動時に完全に削除されます。
+
+
+ 更新あり
+
+
+ インストール済み: {0} → 最新: {1}
+
+
+ インストール済み: {0}
+
+
+ インストール済みバージョン
+
+
+ 最新バージョン
+
+
+ NuGetからのプラグイン一覧の取得に失敗しました。ネットワーク接続を確認してください。
+
+
+ インストール完了
+
+
+ インストール失敗
+
+
+ プラグインの変更を適用するには、WindowTranslatorを再起動してください。
+
+
+ プロジェクトページ
+
+
+ ライセンス情報
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx
index dd98967c..065397e9 100644
--- a/WindowTranslator/Properties/Resources.ru.resx
+++ b/WindowTranslator/Properties/Resources.ru.resx
@@ -447,4 +447,55 @@
+
+ Магазин плагинов
+
+
+ Установить
+
+
+ Установлен
+
+
+ Обновить
+
+
+ Удалить
+
+
+ Вы уверены, что хотите удалить {0}? Он будет полностью удалён при следующем запуске.
+
+
+ Доступно обновление
+
+
+ Установлен: {0} → Последний: {1}
+
+
+ Установлен: {0}
+
+
+ Установленная версия
+
+
+ Последняя версия
+
+
+ Не удалось получить список плагинов из NuGet. Проверьте подключение к сети.
+
+
+ Установка завершена
+
+
+ Ошибка установки
+
+
+ Перезапустите WindowTranslator для применения изменений плагина.
+
+
+ Страница проекта
+
+
+ Информация о лицензии
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx
index efb6f04e..7056a265 100644
--- a/WindowTranslator/Properties/Resources.th.resx
+++ b/WindowTranslator/Properties/Resources.th.resx
@@ -456,4 +456,55 @@
+
+ ร้านปลั๊กอิน
+
+
+ ติดตั้ง
+
+
+ ติดตั้งแล้ว
+
+
+ อัปเดต
+
+
+ ถอนการติดตั้ง
+
+
+ คุณแน่ใจหรือไม่ว่าต้องการถอนการติดตั้ง {0}? จะถูกลบออกอย่างสมบูรณ์เมื่อเริ่มต้นครั้งถัดไป
+
+
+ มีการอัปเดต
+
+
+ ติดตั้งแล้ว: {0} → ล่าสุด: {1}
+
+
+ ติดตั้งแล้ว: {0}
+
+
+ เวอร์ชันที่ติดตั้ง
+
+
+ เวอร์ชันล่าสุด
+
+
+ ไม่สามารถดึงรายการปลั๊กอินจาก NuGet ได้ โปรดตรวจสอบการเชื่อมต่อเครือข่าย
+
+
+ การติดตั้งเสร็จสมบูรณ์
+
+
+ การติดตั้งล้มเหลว
+
+
+ กรุณาเริ่ม WindowTranslator ใหม่เพื่อนำการเปลี่ยนแปลงปลั๊กอินไปใช้
+
+
+ หน้าโครงการ
+
+
+ ข้อมูลใบอนุญาต
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx
index cd9c45fa..b2ec77de 100644
--- a/WindowTranslator/Properties/Resources.tr.resx
+++ b/WindowTranslator/Properties/Resources.tr.resx
@@ -456,4 +456,55 @@ Monitör desteklenmiyor.
+
+ Eklenti Mağazası
+
+
+ Yükle
+
+
+ Yüklü
+
+
+ Güncelle
+
+
+ Kaldır
+
+
+ {0} öğesini kaldırmak istediğinizden emin misiniz? Sonraki başlatmada tamamen silinecek.
+
+
+ Güncelleme mevcut
+
+
+ Yüklü: {0} → En son: {1}
+
+
+ Yüklü: {0}
+
+
+ Yüklü sürüm
+
+
+ En son sürüm
+
+
+ NuGet'ten eklenti listesi alınamadı. Lütfen ağ bağlantınızı kontrol edin.
+
+
+ Kurulum tamamlandı
+
+
+ Kurulum başarısız
+
+
+ Eklenti değişikliklerini uygulamak için lütfen WindowTranslator'ı yeniden başlatın.
+
+
+ Proje sayfası
+
+
+ Lisans bilgileri
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx
index 81d73a70..19e07b60 100644
--- a/WindowTranslator/Properties/Resources.vi.resx
+++ b/WindowTranslator/Properties/Resources.vi.resx
@@ -456,4 +456,55 @@ Màn hình không được hỗ trợ.
+
+ Cửa hàng Plugin
+
+
+ Cài đặt
+
+
+ Đã cài đặt
+
+
+ Cập nhật
+
+
+ Gỡ cài đặt
+
+
+ Bạn có chắc muốn gỡ cài đặt {0}? Nó sẽ được xóa hoàn toàn khi khởi động lại.
+
+
+ Có cập nhật
+
+
+ Đã cài: {0} → Mới nhất: {1}
+
+
+ Đã cài: {0}
+
+
+ Phiên bản đã cài
+
+
+ Phiên bản mới nhất
+
+
+ Không thể lấy danh sách plugin từ NuGet. Vui lòng kiểm tra kết nối mạng.
+
+
+ Cài đặt hoàn tất
+
+
+ Cài đặt thất bại
+
+
+ Vui lòng khởi động lại WindowTranslator để áp dụng thay đổi plugin.
+
+
+ Trang dự án
+
+
+ Thông tin giấy phép
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx
index feb1e119..0699b4d5 100644
--- a/WindowTranslator/Properties/Resources.zh-CN.resx
+++ b/WindowTranslator/Properties/Resources.zh-CN.resx
@@ -456,4 +456,55 @@
+
+ 插件商店
+
+
+ 安装
+
+
+ 已安装
+
+
+ 更新
+
+
+ 卸载
+
+
+ 确定要卸载 {0} 吗?下次启动时将完全移除。
+
+
+ 有更新
+
+
+ 已安装: {0} → 最新: {1}
+
+
+ 已安装: {0}
+
+
+ 已安装版本
+
+
+ 最新版本
+
+
+ 无法从 NuGet 获取插件列表。请检查您的网络连接。
+
+
+ 安装完成
+
+
+ 安装失败
+
+
+ 请重启 WindowTranslator 以应用插件更改。
+
+
+ 项目页面
+
+
+ 许可证信息
+
\ No newline at end of file
diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx
index 784cc9d9..8f4708b8 100644
--- a/WindowTranslator/Properties/Resources.zh-TW.resx
+++ b/WindowTranslator/Properties/Resources.zh-TW.resx
@@ -456,4 +456,55 @@
+
+ 外掛程式商店
+
+
+ 安裝
+
+
+ 已安裝
+
+
+ 更新
+
+
+ 解除安裝
+
+
+ 確定要解除安裝 {0} 嗎?下次啟動時將完全移除。
+
+
+ 有更新
+
+
+ 已安裝: {0} → 最新: {1}
+
+
+ 已安裝: {0}
+
+
+ 已安裝版本
+
+
+ 最新版本
+
+
+ 無法從 NuGet 取得外掛程式清單。請檢查您的網路連線。
+
+
+ 安裝完成
+
+
+ 安裝失敗
+
+
+ 請重新啟動 WindowTranslator 以套用外掛程式變更。
+
+
+ 專案頁面
+
+
+ 授權資訊
+
\ No newline at end of file
From 1364f7aa872061a6a0cbc33b1e3f99a9d0db1c08 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 2 May 2026 14:46:49 +0000
Subject: [PATCH 03/43] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?=
=?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=BC=E3=83=89=E3=83=90=E3=83=83=E3=82=AF?=
=?UTF-8?q?=E5=AF=BE=E5=BF=9C=E3=81=A8=E3=82=B5=E3=83=B3=E3=83=97=E3=83=AB?=
=?UTF-8?q?=E3=83=97=E3=83=AD=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E8=BF=BD?=
=?UTF-8?q?=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Agent-Logs-Url: https://github.com/Freeesia/WindowTranslator/sessions/5fefad82-801f-4158-ad6f-9d7500bf50ed
Co-authored-by: Freeesia <9002657+Freeesia@users.noreply.github.com>
---
Samples/README.md | 135 ++++++++++++++++++
.../SampleTranslateModule.cs | 39 +++++
.../WindowTranslator.Plugin.Sample.csproj | 35 +++++
.../Modules/PluginStore/NuGetPluginService.cs | 5 +-
WindowTranslator/Properties/Resources.hi.resx | 34 ++---
5 files changed, 230 insertions(+), 18 deletions(-)
create mode 100644 Samples/README.md
create mode 100644 Samples/WindowTranslator.Plugin.Sample/SampleTranslateModule.cs
create mode 100644 Samples/WindowTranslator.Plugin.Sample/WindowTranslator.Plugin.Sample.csproj
diff --git a/Samples/README.md b/Samples/README.md
new file mode 100644
index 00000000..0b0fe1b7
--- /dev/null
+++ b/Samples/README.md
@@ -0,0 +1,135 @@
+# WindowTranslator 外部プラグイン開発ガイド
+
+## 概要
+
+WindowTranslator は外部プラグインによる機能拡張をサポートしています。
+NuGet パッケージとしてプラグインを公開することで、他のユーザーがアプリ内から簡単にインストールできます。
+
+## クイックスタート
+
+### 1. プロジェクト作成
+
+```bash
+dotnet new classlib -n WindowTranslator.Plugin.YourPlugin
+cd WindowTranslator.Plugin.YourPlugin
+```
+
+### 2. .csproj を設定
+
+最小構成の `.csproj` 例:
+
+```xml
+
+
+ net10.0
+ true
+
+
+ WindowTranslator.Plugin.YourPlugin
+ 1.0.0
+ YourName
+ 説明文
+
+ windowtranslator-plugin
+ MIT
+
+
+
+
+
+
+
+
+
+
+
+```
+
+> **重要**: `` に `windowtranslator-plugin` を含めることで、
+> WindowTranslator アプリ内のプラグインストアに表示されます。
+
+### 3. プラグインを実装
+
+対象のインターフェースを実装します:
+
+| インターフェース | 用途 |
+|---|---|
+| `ITranslateModule` | テキスト翻訳 |
+| `IOcrModule` | 画像からテキスト認識 |
+| `ICaptureModule` | ウィンドウキャプチャ |
+| `IFilterModule` | 翻訳前後のテキスト加工 |
+| `IColorModule` | 色変換 |
+| `ICacheModule` | 翻訳結果キャッシュ |
+
+```csharp
+using System.ComponentModel;
+using WindowTranslator.Modules;
+
+[DisplayName("MyPlugin 翻訳")]
+public class MyTranslateModule : ITranslateModule
+{
+ public async IAsyncEnumerable TranslateAsync(
+ IAsyncEnumerable texts,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await foreach (var text in texts.WithCancellation(cancellationToken))
+ {
+ yield return await MyTranslateApiAsync(text, cancellationToken);
+ }
+ }
+
+ public ValueTask DisposeAsync() => ValueTask.CompletedTask;
+}
+```
+
+### 4. パッケージをビルドして NuGet に公開
+
+```bash
+dotnet pack -c Release -o ./nupkg
+dotnet nuget push ./nupkg/WindowTranslator.Plugin.YourPlugin.1.0.0.nupkg \
+ --api-key YOUR_API_KEY \
+ --source https://api.nuget.org/v3/index.json
+```
+
+## プラグイン設定パラメータ
+
+プラグインに設定画面を追加するには `IPluginParam` を実装します:
+
+```csharp
+using PropertyTools.DataAnnotations;
+using WindowTranslator;
+
+public class MyPluginParam : IPluginParam
+{
+ [Category("API設定")]
+ [DisplayName("APIキー")]
+ public string ApiKey { get; set; } = string.Empty;
+
+ [Category("翻訳設定")]
+ [DisplayName("翻訳元言語")]
+ public string SourceLanguage { get; set; } = "ja";
+}
+```
+
+## デフォルトモジュールの指定
+
+プラグインをデフォルトとして使用させるには `[DefaultModule]` 属性を付与します:
+
+```csharp
+[DefaultModule]
+[DisplayName("My 翻訳")]
+public class MyTranslateModule : ITranslateModule { ... }
+```
+
+## プラグインインストール先
+
+インストールされたプラグインは以下のフォルダに配置されます:
+
+- Windows: `%USERPROFILE%\.wt\plugins\{PackageId}\`
+
+## 注意事項
+
+- プラグインは .NET 10 以上をターゲットにしてください
+- `true` を必ず設定してください
+- ホスト側で既に提供されているパッケージは `ExcludeAssets="runtime"` を設定し、DLL を重複させないようにしてください
+- プラグインに必要な独自の依存 DLL はすべて `lib/net10.0/` フォルダに含めてください
diff --git a/Samples/WindowTranslator.Plugin.Sample/SampleTranslateModule.cs b/Samples/WindowTranslator.Plugin.Sample/SampleTranslateModule.cs
new file mode 100644
index 00000000..59b81a4b
--- /dev/null
+++ b/Samples/WindowTranslator.Plugin.Sample/SampleTranslateModule.cs
@@ -0,0 +1,39 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Microsoft.Extensions.Logging;
+using WindowTranslator.Modules;
+
+namespace WindowTranslator.Plugin.Sample;
+
+///
+/// サンプル翻訳モジュールです。
+/// テキストをそのまま返す(翻訳しない)実装例です。
+///
+[DisplayName("サンプル翻訳")]
+public class SampleTranslateModule : ITranslateModule
+{
+ private readonly ILogger logger;
+
+ public SampleTranslateModule(ILogger logger)
+ {
+ this.logger = logger;
+ }
+
+ ///
+ public async IAsyncEnumerable TranslateAsync(
+ IAsyncEnumerable texts,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ await foreach (var text in texts.WithCancellation(cancellationToken))
+ {
+ this.logger.LogDebug("翻訳: {Text}", text);
+ // TODO: ここで実際の翻訳処理を実装してください
+ yield return $"[翻訳済み] {text}";
+ }
+ }
+
+ public ValueTask DisposeAsync()
+ {
+ return ValueTask.CompletedTask;
+ }
+}
diff --git a/Samples/WindowTranslator.Plugin.Sample/WindowTranslator.Plugin.Sample.csproj b/Samples/WindowTranslator.Plugin.Sample/WindowTranslator.Plugin.Sample.csproj
new file mode 100644
index 00000000..7bb0e01b
--- /dev/null
+++ b/Samples/WindowTranslator.Plugin.Sample/WindowTranslator.Plugin.Sample.csproj
@@ -0,0 +1,35 @@
+
+
+
+
+ net10.0
+
+ true
+
+
+ WindowTranslator.Plugin.Sample
+ 1.0.0
+ YourName
+ WindowTranslator サンプルプラグイン
+ windowtranslator-plugin
+ https://github.com/yourname/windowtranslator-plugin-sample
+ MIT
+ false
+
+
+
+
+
+
+
+
+ false
+ runtime
+
+
+
+
+
+
+
+
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 3f0c81bb..5e01aa89 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -33,7 +33,10 @@ public sealed class NuGetPluginService : IDisposable
public NuGetPluginService(ILogger logger)
{
- this.httpClient = new HttpClient();
+ this.httpClient = new HttpClient
+ {
+ Timeout = TimeSpan.FromSeconds(30),
+ };
this.logger = logger;
}
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index ba7b6383..2eb684cc 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -456,54 +456,54 @@
- Plugin Store
+ प्लगइन स्टोर
- Install
+ इंस्टॉल करें
- Installed
+ इंस्टॉल है
- Update
+ अपडेट करें
- Uninstall
+ अनइंस्टॉल करें
- Are you sure you want to uninstall {0}? It will be fully removed on next startup.
+ क्या आप {0} को अनइंस्टॉल करना चाहते हैं? यह अगली बार शुरू होने पर पूरी तरह हटा दिया जाएगा।
- Update available
+ अपडेट उपलब्ध
- Installed: {0} → Latest: {1}
+ इंस्टॉल: {0} → नवीनतम: {1}
- Installed: {0}
+ इंस्टॉल: {0}
- Installed version
+ इंस्टॉल किया गया संस्करण
- Latest version
+ नवीनतम संस्करण
- Failed to retrieve plugin list from NuGet. Please check your network connection.
+ NuGet से प्लगइन सूची प्राप्त करने में विफल। कृपया अपना नेटवर्क कनेक्शन जांचें।
- Installation complete
+ इंस्टॉलेशन पूर्ण
- Installation failed
+ इंस्टॉलेशन विफल
- Please restart WindowTranslator to apply plugin changes.
+ प्लगइन परिवर्तन लागू करने के लिए कृपया WindowTranslator को पुनः आरंभ करें।
- Project page
+ प्रोजेक्ट पेज
- License information
+ लाइसेंस जानकारी
\ No newline at end of file
From fb90a494f08c4334ce84d40128fe9d97a7dd9587 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 2 May 2026 15:13:15 +0000
Subject: [PATCH 04/43] Changes before error encountered
Agent-Logs-Url: https://github.com/Freeesia/WindowTranslator/sessions/49085e31-7c44-4f11-82e0-fe40172b8447
Co-authored-by: Freeesia <9002657+Freeesia@users.noreply.github.com>
---
Plugins/Directory.Build.props | 9 ++
Plugins/Directory.Build.targets | 2 +-
...nslator.Plugin.DeepLTranslatePlugin.csproj | 4 +
.../WindowTranslator.Plugin.FoMPlugin.csproj | 1 +
...anslator.Plugin.GitHubCopilotPlugin.csproj | 1 +
...dowTranslator.Plugin.GoogleAIPlugin.csproj | 1 +
...lator.Plugin.GoogleAppsSctiptPlugin.csproj | 3 +
.../WindowTranslator.Plugin.LLMPlugin.csproj | 1 +
...WindowTranslator.Plugin.PLaMoPlugin.csproj | 1 +
...ranslator.Plugin.TesseractOCRPlugin.csproj | 1 +
.../Modules/PluginStore/NuGetPluginCatalog.cs | 53 ++++++++
.../Modules/PluginStore/NuGetPluginService.cs | 127 ++++++++++++++++++
WindowTranslator/Program.cs | 18 ++-
13 files changed, 214 insertions(+), 8 deletions(-)
create mode 100644 WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
diff --git a/Plugins/Directory.Build.props b/Plugins/Directory.Build.props
index ffcb5d5e..eaff4328 100644
--- a/Plugins/Directory.Build.props
+++ b/Plugins/Directory.Build.props
@@ -6,6 +6,15 @@
true
+
+
+ windowtranslator-plugin
+ https://github.com/Freeesia/WindowTranslator
+ Freeesia
+
+ false
+
+
diff --git a/Plugins/Directory.Build.targets b/Plugins/Directory.Build.targets
index 24869907..0721539f 100644
--- a/Plugins/Directory.Build.targets
+++ b/Plugins/Directory.Build.targets
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
index 1cd83d92..8c02a33f 100644
--- a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
@@ -1,5 +1,9 @@
+
+ true
+
+
diff --git a/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj b/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
index 16bac979..e0c9fe00 100644
--- a/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
@@ -2,6 +2,7 @@
net10.0-windows10.0.20348.0
true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
index 90dd527f..e9ea1352 100644
--- a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
@@ -1,6 +1,7 @@
net10.0-windows10.0.20348.0
+ true
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
index ecc7110a..e15c28c5 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
@@ -2,6 +2,7 @@
net10.0-windows10.0.20348.0
+ true
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
index e09e0417..6177f6ba 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
@@ -1,4 +1,7 @@
+
+ true
+
diff --git a/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj b/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
index 33feedb9..a37c1d24 100644
--- a/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
@@ -1,6 +1,7 @@
net10.0-windows10.0.20348.0
+ true
diff --git a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
index c1a0b6bd..bc77e53a 100644
--- a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
@@ -2,6 +2,7 @@
net10.0
+ true
diff --git a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
index bcae45af..b0e61f0e 100644
--- a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
@@ -2,6 +2,7 @@
net10.0-windows10.0.20348.0
+ true
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
new file mode 100644
index 00000000..fac75a44
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -0,0 +1,53 @@
+using System.IO;
+using Weikio.PluginFramework.Catalogs;
+
+namespace WindowTranslator.Modules.PluginStore;
+
+///
+/// NuGet経由でインストールされたプラグインを一時フォルダからロードするカタログです。
+/// ファイルロックを回避するため、読み込み前にプラグインフォルダを一時フォルダにコピーします。
+///
+public class NuGetPluginCatalog(string sourceDir, FolderPluginCatalogOptions options)
+ : FolderPluginCatalog(
+ Path.Combine(Path.GetTempPath(), "WindowTranslator", "plugins"),
+ options)
+{
+ private static readonly string TempDir =
+ Path.Combine(Path.GetTempPath(), "WindowTranslator", "plugins");
+
+ ///
+ public override async Task Initialize()
+ {
+ // ロック解除のために一時フォルダを削除してからコピー
+ if (Directory.Exists(TempDir))
+ {
+ Directory.Delete(TempDir, recursive: true);
+ }
+ Directory.CreateDirectory(TempDir);
+
+ if (Directory.Exists(sourceDir))
+ {
+ // プラグインのサブフォルダのみコピー(nuget-manifest.json等のファイルはスキップ)
+ foreach (var subDir in Directory.GetDirectories(sourceDir))
+ {
+ var destSubDir = Path.Combine(TempDir, Path.GetFileName(subDir));
+ CopyDirectory(subDir, destSubDir);
+ }
+ }
+
+ await base.Initialize().ConfigureAwait(false);
+ }
+
+ private static void CopyDirectory(string source, string destination)
+ {
+ Directory.CreateDirectory(destination);
+ foreach (var file in Directory.GetFiles(source))
+ {
+ File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), overwrite: true);
+ }
+ foreach (var subDir in Directory.GetDirectories(source))
+ {
+ CopyDirectory(subDir, Path.Combine(destination, Path.GetFileName(subDir)));
+ }
+ }
+}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 5e01aa89..552ab054 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -16,6 +16,40 @@ public sealed class NuGetPluginService : IDisposable
private const string PluginTag = "windowtranslator-plugin";
private const string NuGetFlatContainerBase = "https://api.nuget.org/v3-flatcontainer";
+ ///
+ /// モジュール/パラメータクラス名からNuGetパッケージIDへのマッピング。
+ /// アプリバンドルから除外されたプラグインの後方互換性自動インストールに使用します。
+ ///
+ public static readonly IReadOnlyDictionary KnownClassToPackage =
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ // WindowTranslator.Plugin.FoMPlugin
+ ["FoMFilterModule"] = "WindowTranslator.Plugin.FoMPlugin",
+ ["FoMOptions"] = "WindowTranslator.Plugin.FoMPlugin",
+ // WindowTranslator.Plugin.PLaMoPlugin
+ ["PLaMoTranslator"] = "WindowTranslator.Plugin.PLaMoPlugin",
+ ["PLaMoOptions"] = "WindowTranslator.Plugin.PLaMoPlugin",
+ // WindowTranslator.Plugin.GitHubCopilotPlugin
+ ["GitHubCopilotTranslator"] = "WindowTranslator.Plugin.GitHubCopilotPlugin",
+ ["GitHubCopilotOptions"] = "WindowTranslator.Plugin.GitHubCopilotPlugin",
+ // WindowTranslator.Plugin.DeepLTranslatePlugin
+ ["DeepLTranslator"] = "WindowTranslator.Plugin.DeepLTranslatePlugin",
+ ["DeepLOptions"] = "WindowTranslator.Plugin.DeepLTranslatePlugin",
+ // WindowTranslator.Plugin.GoogleAIPlugin
+ ["GoogleAITranslator"] = "WindowTranslator.Plugin.GoogleAIPlugin",
+ ["GoogleAIOcr"] = "WindowTranslator.Plugin.GoogleAIPlugin",
+ ["GoogleAIOptions"] = "WindowTranslator.Plugin.GoogleAIPlugin",
+ // WindowTranslator.Plugin.GoogleAppsSctiptPlugin
+ ["GasTranslator"] = "WindowTranslator.Plugin.GoogleAppsSctiptPlugin",
+ ["GasOptions"] = "WindowTranslator.Plugin.GoogleAppsSctiptPlugin",
+ // WindowTranslator.Plugin.LLMPlugin
+ ["LLMTranslator"] = "WindowTranslator.Plugin.LLMPlugin",
+ ["LLMOcr"] = "WindowTranslator.Plugin.LLMPlugin",
+ ["LLMOptions"] = "WindowTranslator.Plugin.LLMPlugin",
+ // WindowTranslator.Plugin.TesseractOCRPlugin
+ ["TesseractOcr"] = "WindowTranslator.Plugin.TesseractOCRPlugin",
+ };
+
private static readonly string UserPluginsDir = Path.Combine(PathUtility.UserDir, "plugins");
private static readonly string ManifestPath = Path.Combine(UserPluginsDir, "nuget-manifest.json");
@@ -40,6 +74,95 @@ public NuGetPluginService(ILogger logger)
this.logger = logger;
}
+ ///
+ /// 指定したパッケージの最新バージョンをインストールします。
+ ///
+ public async Task InstallLatestPackageAsync(string packageId, IProgress? progress = null, CancellationToken cancellationToken = default)
+ {
+ var versionsUrl = $"{NuGetFlatContainerBase}/{packageId.ToLowerInvariant()}/index.json";
+ var response = await this.httpClient.GetAsync(versionsUrl, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var versions = await JsonSerializer.DeserializeAsync(content, JsonOptions, cancellationToken).ConfigureAwait(false);
+ var latestVersion = versions?.Versions?.LastOrDefault()
+ ?? throw new InvalidOperationException($"パッケージ {packageId} のバージョン一覧を取得できませんでした。");
+ await InstallPackageAsync(packageId, latestVersion, progress, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// 設定ファイルで参照されているがインストールされていないプラグインを自動インストールします。
+ /// アプリバンドルから除外されたプラグインの後方互換性維持のために使用します。
+ ///
+ public async Task AutoInstallFromSettingsAsync(string settingsPath, CancellationToken cancellationToken = default)
+ {
+ if (!File.Exists(settingsPath))
+ {
+ return;
+ }
+
+ try
+ {
+ using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(settingsPath, cancellationToken).ConfigureAwait(false));
+ var neededPackages = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ if (doc.RootElement.TryGetProperty("Targets", out var targets))
+ {
+ foreach (var target in targets.EnumerateObject())
+ {
+ // SelectedPlugins の値(モジュールクラス名)をチェック
+ if (target.Value.TryGetProperty("SelectedPlugins", out var selectedPlugins))
+ {
+ foreach (var plugin in selectedPlugins.EnumerateObject())
+ {
+ var className = plugin.Value.GetString();
+ if (className is not null && KnownClassToPackage.TryGetValue(className, out var packageId))
+ {
+ neededPackages.Add(packageId);
+ }
+ }
+ }
+
+ // PluginParams のキー(パラメータクラス名)をチェック
+ if (target.Value.TryGetProperty("PluginParams", out var pluginParams))
+ {
+ foreach (var param in pluginParams.EnumerateObject())
+ {
+ if (KnownClassToPackage.TryGetValue(param.Name, out var packageId))
+ {
+ neededPackages.Add(packageId);
+ }
+ }
+ }
+ }
+ }
+
+ if (neededPackages.Count == 0)
+ {
+ return;
+ }
+
+ var installed = await GetInstalledPackagesAsync(cancellationToken).ConfigureAwait(false);
+ var installedIds = installed.Select(p => p.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var packageId in neededPackages.Where(id => !installedIds.Contains(id)))
+ {
+ this.logger.LogInformation("設定で参照されているプラグインを自動インストール: {PackageId}", packageId);
+ try
+ {
+ await InstallLatestPackageAsync(packageId, cancellationToken: cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogWarning(ex, "プラグイン {PackageId} の自動インストールに失敗しました。", packageId);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogWarning(ex, "設定からのプラグイン自動インストール処理中にエラーが発生しました。");
+ }
+ }
+
///
/// NuGetでWindowTranslatorプラグインを検索します。
///
@@ -394,3 +517,7 @@ internal record NuGetSearchData(
[property: JsonPropertyName("projectUrl")] string? ProjectUrl,
[property: JsonPropertyName("licenseUrl")] string? LicenseUrl
);
+
+internal record NuGetVersionListResponse(
+ [property: JsonPropertyName("versions")] string[]? Versions
+);
diff --git a/WindowTranslator/Program.cs b/WindowTranslator/Program.cs
index 7296b8be..6c2bab4a 100644
--- a/WindowTranslator/Program.cs
+++ b/WindowTranslator/Program.cs
@@ -59,6 +59,14 @@
var exeDir = Path.GetDirectoryName(Environment.GetCommandLineArgs()[0])!;
Directory.SetCurrentDirectory(exeDir);
+// ペンディング削除処理と設定参照プラグインの自動インストール(カタログ初期化より前に実行する必要がある)
+{
+ using var earlyLoggerFactory = LoggerFactory.Create(b => b.SetMinimumLevel(LogLevel.Warning));
+ using var earlyNuGetService = new NuGetPluginService(earlyLoggerFactory.CreateLogger());
+ earlyNuGetService.ProcessPendingDeletions();
+ await earlyNuGetService.AutoInstallFromSettingsAsync(PathUtility.UserSettings);
+}
+
var builder = KamishibaiApplication.CreateBuilder();
builder.Host.ConfigureLogging((c, l) =>
@@ -122,10 +130,9 @@
}
var userPluginsDir = Path.Combine(PathUtility.UserDir, "plugins");
-if (Directory.Exists(userPluginsDir))
-{
- pluginFolderCatalog.AddCatalog(new FolderPluginCatalog(userPluginsDir, options: new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } }));
-}
+pluginFolderCatalog.AddCatalog(new NuGetPluginCatalog(
+ userPluginsDir,
+ new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } }));
builder.Services.AddPluginCatalog(pluginFolderCatalog);
builder.Configuration
@@ -188,9 +195,6 @@
e.Window.Activate();
};
-// 起動時にペンディング削除を処理する
-app.Services.GetRequiredService().ProcessPendingDeletions();
-
if (SentrySdk.IsEnabled)
{
app.Logger.LogInformation("Sentry is enabled.");
From 37f3588e93124ff41650e858931d4ef7e45d8ca6 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sun, 26 Jul 2026 15:10:12 +0900
Subject: [PATCH 05/43] =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC?=
=?UTF-8?q?=E6=8C=87=E6=91=98=E3=81=AE=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E9=85=8D=E5=B8=83=E5=8B=95=E4=BD=9C=E3=82=92=E4=BF=AE?=
=?UTF-8?q?=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Plugins/Directory.Build.props | 4 +-
Plugins/Directory.Build.targets | 4 +-
.../SampleTranslateModule.cs | 39 -------------------
.../WindowTranslator.Plugin.Sample.csproj | 35 -----------------
.../Modules/PluginStore/NuGetPluginCatalog.cs | 8 +++-
Samples/README.md => docs/plugin.md | 25 +++---------
6 files changed, 16 insertions(+), 99 deletions(-)
delete mode 100644 Samples/WindowTranslator.Plugin.Sample/SampleTranslateModule.cs
delete mode 100644 Samples/WindowTranslator.Plugin.Sample/WindowTranslator.Plugin.Sample.csproj
rename Samples/README.md => docs/plugin.md (86%)
diff --git a/Plugins/Directory.Build.props b/Plugins/Directory.Build.props
index eaff4328..2b796eb3 100644
--- a/Plugins/Directory.Build.props
+++ b/Plugins/Directory.Build.props
@@ -11,7 +11,7 @@
windowtranslator-plugin
https://github.com/Freeesia/WindowTranslator
Freeesia
-
+
false
@@ -31,4 +31,4 @@
runtime
-
\ No newline at end of file
+
diff --git a/Plugins/Directory.Build.targets b/Plugins/Directory.Build.targets
index 0721539f..efca3a7f 100644
--- a/Plugins/Directory.Build.targets
+++ b/Plugins/Directory.Build.targets
@@ -2,7 +2,7 @@
-
+
-
\ No newline at end of file
+
diff --git a/Samples/WindowTranslator.Plugin.Sample/SampleTranslateModule.cs b/Samples/WindowTranslator.Plugin.Sample/SampleTranslateModule.cs
deleted file mode 100644
index 59b81a4b..00000000
--- a/Samples/WindowTranslator.Plugin.Sample/SampleTranslateModule.cs
+++ /dev/null
@@ -1,39 +0,0 @@
-using System.ComponentModel;
-using System.Runtime.CompilerServices;
-using Microsoft.Extensions.Logging;
-using WindowTranslator.Modules;
-
-namespace WindowTranslator.Plugin.Sample;
-
-///
-/// サンプル翻訳モジュールです。
-/// テキストをそのまま返す(翻訳しない)実装例です。
-///
-[DisplayName("サンプル翻訳")]
-public class SampleTranslateModule : ITranslateModule
-{
- private readonly ILogger logger;
-
- public SampleTranslateModule(ILogger logger)
- {
- this.logger = logger;
- }
-
- ///
- public async IAsyncEnumerable TranslateAsync(
- IAsyncEnumerable texts,
- [EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- await foreach (var text in texts.WithCancellation(cancellationToken))
- {
- this.logger.LogDebug("翻訳: {Text}", text);
- // TODO: ここで実際の翻訳処理を実装してください
- yield return $"[翻訳済み] {text}";
- }
- }
-
- public ValueTask DisposeAsync()
- {
- return ValueTask.CompletedTask;
- }
-}
diff --git a/Samples/WindowTranslator.Plugin.Sample/WindowTranslator.Plugin.Sample.csproj b/Samples/WindowTranslator.Plugin.Sample/WindowTranslator.Plugin.Sample.csproj
deleted file mode 100644
index 7bb0e01b..00000000
--- a/Samples/WindowTranslator.Plugin.Sample/WindowTranslator.Plugin.Sample.csproj
+++ /dev/null
@@ -1,35 +0,0 @@
-
-
-
-
- net10.0
-
- true
-
-
- WindowTranslator.Plugin.Sample
- 1.0.0
- YourName
- WindowTranslator サンプルプラグイン
- windowtranslator-plugin
- https://github.com/yourname/windowtranslator-plugin-sample
- MIT
- false
-
-
-
-
-
-
-
-
- false
- runtime
-
-
-
-
-
-
-
-
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 20e4f98f..f961c801 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -59,7 +59,13 @@ private static void CopyDirectory(string source, string destination)
Directory.CreateDirectory(destination);
foreach (var file in Directory.GetFiles(source))
{
- File.Copy(file, Path.Combine(destination, Path.GetFileName(file)), overwrite: true);
+ var destinationFile = Path.Combine(destination, Path.GetFileName(file));
+ if (File.Exists(destinationFile))
+ {
+ continue;
+ }
+
+ File.Copy(file, destinationFile);
}
foreach (var subDir in Directory.GetDirectories(source))
{
diff --git a/Samples/README.md b/docs/plugin.md
similarity index 86%
rename from Samples/README.md
rename to docs/plugin.md
index 0b0fe1b7..cc9f31c6 100644
--- a/Samples/README.md
+++ b/docs/plugin.md
@@ -61,26 +61,11 @@ cd WindowTranslator.Plugin.YourPlugin
| `IColorModule` | 色変換 |
| `ICacheModule` | 翻訳結果キャッシュ |
-```csharp
-using System.ComponentModel;
-using WindowTranslator.Modules;
-
-[DisplayName("MyPlugin 翻訳")]
-public class MyTranslateModule : ITranslateModule
-{
- public async IAsyncEnumerable TranslateAsync(
- IAsyncEnumerable texts,
- [EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- await foreach (var text in texts.WithCancellation(cancellationToken))
- {
- yield return await MyTranslateApiAsync(text, cancellationToken);
- }
- }
-
- public ValueTask DisposeAsync() => ValueTask.CompletedTask;
-}
-```
+実際の実装例は
+[DeepLTranslator.cs](../Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/DeepLTranslator.cs)
+と
+[WindowTranslator.Plugin.DeepLTranslatePlugin.csproj](../Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj)
+を参照してください。
### 4. パッケージをビルドして NuGet に公開
From 61ee76d8beed0beaca1ed638ac6191da0771fda5 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Tue, 28 Jul 2026 21:50:16 +0900
Subject: [PATCH 06/43] =?UTF-8?q?NuGet=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E9=85=8D=E5=B8=83=E3=81=A8=E4=BE=9D=E5=AD=98=E8=A7=A3?=
=?UTF-8?q?=E6=B1=BA=E3=82=92=E6=94=B9=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/dotnet-package.yml | 15 +
Directory.Packages.props | 3 +-
Plugins/Directory.Build.props | 3 +-
Plugins/Directory.Build.targets | 23 +
.../NuGetPluginServiceTests.cs | 521 +++++++++++++++
.../PluginStore/NuGetPackageInstaller.cs | 594 ++++++++++++++++++
.../Modules/PluginStore/NuGetPluginCatalog.cs | 72 ++-
.../Modules/PluginStore/NuGetPluginService.cs | 489 +++++++++-----
.../PluginStore/PluginStoreViewModel.cs | 12 +-
WindowTranslator/Properties/AssemblyInfo.cs | 3 +
WindowTranslator/WindowTranslator.csproj | 1 +
docs/plugin.md | 16 +-
12 files changed, 1556 insertions(+), 196 deletions(-)
create mode 100644 WindowTranslator.Tests/NuGetPluginServiceTests.cs
create mode 100644 WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
create mode 100644 WindowTranslator/Properties/AssemblyInfo.cs
diff --git a/.github/workflows/dotnet-package.yml b/.github/workflows/dotnet-package.yml
index 2f73507f..32314746 100644
--- a/.github/workflows/dotnet-package.yml
+++ b/.github/workflows/dotnet-package.yml
@@ -25,10 +25,25 @@ jobs:
versionSpec: "6.x"
- id: gitversion
uses: gittools/actions/gitversion/execute@v4.7.0
+ - uses: Jimver/cuda-toolkit@v0.2.30
+ with:
+ cuda: '12.9.0'
- run: |
dotnet pack WindowTranslator.Abstractions -c Release -o pack `
-p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
+ Get-ChildItem Plugins\WindowTranslator.Plugin.*\*.csproj |
+ Where-Object { $_.Directory.Name -notlike '*.Tests' } |
+ ForEach-Object {
+ dotnet pack $_.FullName -c Release -o pack `
+ -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
+ -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
+ -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
+ -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+ }
dotnet nuget push pack\*.nupkg -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 3d409406..512bce90 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -34,6 +34,7 @@
+
@@ -59,4 +60,4 @@
-
\ No newline at end of file
+
diff --git a/Plugins/Directory.Build.props b/Plugins/Directory.Build.props
index 2b796eb3..3295561e 100644
--- a/Plugins/Directory.Build.props
+++ b/Plugins/Directory.Build.props
@@ -8,9 +8,10 @@
- windowtranslator-plugin
https://github.com/Freeesia/WindowTranslator
Freeesia
+
+ false
false
diff --git a/Plugins/Directory.Build.targets b/Plugins/Directory.Build.targets
index efca3a7f..ed78c408 100644
--- a/Plugins/Directory.Build.targets
+++ b/Plugins/Directory.Build.targets
@@ -2,6 +2,29 @@
+
+ $(PackageTags);windowtranslator-plugin
+ $(TargetsForTfmSpecificBuildOutput);AddPluginRuntimeAssetsToPackage
+
+
+
+
+
+ <_PluginWinX64RuntimeAsset Include="$(TargetDir)runtimes\win-x64\**\*" />
+ <_PluginWinRuntimeAsset Include="$(TargetDir)runtimes\win\**\*" />
+ <_PluginAnyRuntimeAsset Include="$(TargetDir)runtimes\any\**\*" />
+
+
+
+
+
+
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
new file mode 100644
index 00000000..c87f2313
--- /dev/null
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -0,0 +1,521 @@
+using System.IO.Compression;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Text.Json;
+using System.Xml.Linq;
+using Microsoft.Extensions.Logging.Abstractions;
+using WindowTranslator.Modules.PluginStore;
+
+namespace WindowTranslator.Tests;
+
+public sealed class NuGetPluginServiceTests
+{
+ [Fact]
+ public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Root.Plugin",
+ "1.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "1.0.0",
+ [
+ new("Dependency.Package", "[1.0.0, 2.0.0)"),
+ new("Host.Provided", "[1.0.0]", Exclude: "Runtime"),
+ ],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "root"u8.ToArray(),
+ ["lib/net10.0/fr/Root.Plugin.resources.dll"] = "fr"u8.ToArray(),
+ ["runtimes/win-x64/native/root-native.dll"] = "native"u8.ToArray(),
+ ["lib/net10.0/runtimes/win-x64/native/custom-native.dll"] = "custom"u8.ToArray(),
+ }));
+ handler.AddPackage(
+ "Dependency.Package",
+ "1.0.0",
+ CreatePackage(
+ "Dependency.Package",
+ "1.0.0",
+ [new("Transitive.Package", "[2.0.0]")],
+ new Dictionary
+ {
+ ["lib/net8.0/Dependency.Package.dll"] = "dependency"u8.ToArray(),
+ }));
+ handler.AddPackage(
+ "Transitive.Package",
+ "2.0.0",
+ CreatePackage(
+ "Transitive.Package",
+ "2.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/netstandard2.0/Transitive.Package.dll"] = "transitive"u8.ToArray(),
+ }));
+
+ using var client = new HttpClient(handler);
+ using var service = CreateService(client, testDirectory);
+
+ await service.InstallPackageAsync("Root.Plugin", "1.0.0");
+
+ var pluginDirectory = Path.Combine(testDirectory, "Root.Plugin");
+ Assert.Equal("root", await File.ReadAllTextAsync(Path.Combine(pluginDirectory, "Root.Plugin.dll")));
+ Assert.Equal(
+ "fr",
+ await File.ReadAllTextAsync(Path.Combine(pluginDirectory, "fr", "Root.Plugin.resources.dll")));
+ Assert.Equal(
+ "dependency",
+ await File.ReadAllTextAsync(Path.Combine(pluginDirectory, "Dependency.Package.dll")));
+ Assert.Equal(
+ "transitive",
+ await File.ReadAllTextAsync(Path.Combine(pluginDirectory, "Transitive.Package.dll")));
+ Assert.Equal(
+ "native",
+ await File.ReadAllTextAsync(Path.Combine(
+ pluginDirectory,
+ "runtimes",
+ "win-x64",
+ "native",
+ "root-native.dll")));
+ Assert.Equal(
+ "custom",
+ await File.ReadAllTextAsync(Path.Combine(
+ pluginDirectory,
+ "runtimes",
+ "win-x64",
+ "native",
+ "custom-native.dll")));
+ Assert.DoesNotContain(
+ handler.RequestedPaths,
+ path => path.Contains("host.provided", StringComparison.OrdinalIgnoreCase));
+
+ var installed = await service.GetInstalledPackagesAsync();
+ var package = Assert.Single(installed);
+ Assert.Equal("Root.Plugin", package.Id);
+ Assert.Equal("1.0.0", package.Version);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task ManifestWriteFailureRestoresThePreviousPluginDirectory()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Root.Plugin",
+ "1.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "1.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "version-one"u8.ToArray(),
+ }));
+ handler.AddPackage(
+ "Root.Plugin",
+ "2.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "2.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "version-two"u8.ToArray(),
+ }));
+
+ using var client = new HttpClient(handler);
+ using var service = CreateService(client, testDirectory);
+ await service.InstallPackageAsync("Root.Plugin", "1.0.0");
+
+ var manifestPath = Path.Combine(testDirectory, "nuget-manifest.json");
+ await using (var manifestLock = new FileStream(
+ manifestPath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read))
+ {
+ var exception = await Record.ExceptionAsync(
+ () => service.InstallPackageAsync("Root.Plugin", "2.0.0"));
+ Assert.True(
+ exception is IOException or UnauthorizedAccessException,
+ $"Unexpected exception: {exception}");
+ }
+
+ Assert.Equal(
+ "version-one",
+ await File.ReadAllTextAsync(
+ Path.Combine(testDirectory, "Root.Plugin", "Root.Plugin.dll")));
+ var installed = Assert.Single(await service.GetInstalledPackagesAsync());
+ Assert.Equal("1.0.0", installed.Version);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task ReinstallAfterUninstallRemovesThePendingDeletionMarker()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Root.Plugin",
+ "1.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "1.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "version-one"u8.ToArray(),
+ }));
+ handler.AddPackage(
+ "Root.Plugin",
+ "2.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "2.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "version-two"u8.ToArray(),
+ }));
+
+ using var client = new HttpClient(handler);
+ using var service = CreateService(client, testDirectory);
+ await service.InstallPackageAsync("Root.Plugin", "1.0.0");
+ await service.UninstallPackageAsync("Root.Plugin");
+
+ var markerPath = Path.Combine(testDirectory, "Root.Plugin.pending-delete");
+ Assert.True(File.Exists(markerPath));
+
+ await service.InstallPackageAsync("Root.Plugin", "2.0.0");
+ Assert.False(File.Exists(markerPath));
+
+ service.ProcessPendingDeletions();
+ Assert.True(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+ var installed = Assert.Single(await service.GetInstalledPackagesAsync());
+ Assert.Equal("2.0.0", installed.Version);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task DependencyWithIncompatibleLibStillInstallsCompatibleNativeAssets()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Root.Plugin",
+ "1.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "1.0.0",
+ [new("Native.Dependency", "[1.0.0]")],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "root"u8.ToArray(),
+ }));
+ handler.AddPackage(
+ "Native.Dependency",
+ "1.0.0",
+ CreatePackage(
+ "Native.Dependency",
+ "1.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/net48/LegacyOnly.dll"] = "legacy"u8.ToArray(),
+ ["runtimes/win-x64/native/compatible.dll"] = "native"u8.ToArray(),
+ }));
+
+ using var client = new HttpClient(handler);
+ using var service = CreateService(client, testDirectory);
+
+ await service.InstallPackageAsync("Root.Plugin", "1.0.0");
+
+ Assert.False(File.Exists(
+ Path.Combine(testDirectory, "Root.Plugin", "LegacyOnly.dll")));
+ Assert.Equal(
+ "native",
+ await File.ReadAllTextAsync(Path.Combine(
+ testDirectory,
+ "Root.Plugin",
+ "runtimes",
+ "win-x64",
+ "native",
+ "compatible.dll")));
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task InvalidManifestDoesNotReplaceExistingPluginDirectory()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ var pluginDirectory = Path.Combine(testDirectory, "Root.Plugin");
+ Directory.CreateDirectory(pluginDirectory);
+ await File.WriteAllTextAsync(
+ Path.Combine(pluginDirectory, "Root.Plugin.dll"),
+ "existing");
+ await File.WriteAllTextAsync(
+ Path.Combine(testDirectory, "nuget-manifest.json"),
+ "{ invalid");
+
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Root.Plugin",
+ "2.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "2.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "replacement"u8.ToArray(),
+ }));
+
+ using var client = new HttpClient(handler);
+ using var service = CreateService(client, testDirectory);
+
+ await Assert.ThrowsAsync(
+ () => service.InstallPackageAsync("Root.Plugin", "2.0.0"));
+
+ Assert.Equal(
+ "existing",
+ await File.ReadAllTextAsync(
+ Path.Combine(pluginDirectory, "Root.Plugin.dll")));
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public void CatalogCopyIncludesLegacyRootFilesAndSkipsManagementState()
+ {
+ var sourceDirectory = CreateTestDirectory();
+ var destinationDirectory = CreateTestDirectory();
+ try
+ {
+ File.WriteAllText(Path.Combine(sourceDirectory, "Legacy.Plugin.dll"), "legacy");
+ File.WriteAllText(Path.Combine(sourceDirectory, "nuget-manifest.json"), "{}");
+ File.WriteAllText(Path.Combine(sourceDirectory, "nuget-manifest.json.tmp-test"), "{}");
+ File.WriteAllText(Path.Combine(sourceDirectory, "Root.Plugin.pending-delete"), "Root.Plugin");
+ File.WriteAllText(
+ Path.Combine(sourceDirectory, "Root.Plugin.pending-delete.tmp-test"),
+ "Root.Plugin");
+ Directory.CreateDirectory(Path.Combine(sourceDirectory, "Root.Plugin"));
+ File.WriteAllText(
+ Path.Combine(sourceDirectory, "Root.Plugin", "Root.Plugin.dll"),
+ "plugin");
+ Directory.CreateDirectory(Path.Combine(sourceDirectory, "Root.Plugin.backup-test"));
+ File.WriteAllText(
+ Path.Combine(sourceDirectory, "Root.Plugin.backup-test", "old.dll"),
+ "old");
+ Directory.CreateDirectory(Path.Combine(sourceDirectory, ".Root.Plugin.installing-test"));
+ Directory.CreateDirectory(Path.Combine(destinationDirectory, "Root.Plugin"));
+ File.WriteAllText(
+ Path.Combine(destinationDirectory, "Root.Plugin", "Root.Plugin.dll"),
+ "existing");
+
+ NuGetPluginCatalog.CopyPluginFiles(sourceDirectory, destinationDirectory);
+
+ Assert.True(File.Exists(Path.Combine(destinationDirectory, "Legacy.Plugin.dll")));
+ Assert.Equal(
+ "existing",
+ File.ReadAllText(
+ Path.Combine(destinationDirectory, "Root.Plugin", "Root.Plugin.dll")));
+ Assert.False(File.Exists(Path.Combine(destinationDirectory, "nuget-manifest.json")));
+ Assert.False(File.Exists(
+ Path.Combine(destinationDirectory, "nuget-manifest.json.tmp-test")));
+ Assert.False(File.Exists(Path.Combine(destinationDirectory, "Root.Plugin.pending-delete")));
+ Assert.False(File.Exists(
+ Path.Combine(destinationDirectory, "Root.Plugin.pending-delete.tmp-test")));
+ Assert.False(Directory.Exists(
+ Path.Combine(destinationDirectory, "Root.Plugin.backup-test")));
+ Assert.False(Directory.Exists(
+ Path.Combine(destinationDirectory, ".Root.Plugin.installing-test")));
+ }
+ finally
+ {
+ DeleteTestDirectory(sourceDirectory);
+ DeleteTestDirectory(destinationDirectory);
+ }
+ }
+
+ [Fact]
+ public void FrameworkSelectionPrefersTheCompatibleWindowsTarget()
+ {
+ Assert.Equal(
+ "net10.0-windows10.0.20348.0",
+ NuGetPackageInstaller.SelectBestTfm(
+ ["net10.0", "net10.0-windows10.0.20348.0", "netstandard2.0"]));
+ Assert.Null(NuGetPackageInstaller.SelectBestTfm(["net48"]));
+ }
+
+ private static NuGetPluginService CreateService(HttpClient client, string pluginDirectory)
+ => new(
+ NullLogger.Instance,
+ client,
+ pluginDirectory);
+
+ private static byte[] CreatePackage(
+ string id,
+ string version,
+ IReadOnlyCollection dependencies,
+ IReadOnlyDictionary entries)
+ {
+ using var stream = new MemoryStream();
+ using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true))
+ {
+ var dependencyElements = dependencies.Select(dependency =>
+ {
+ var element = new XElement(
+ "dependency",
+ new XAttribute("id", dependency.Id),
+ new XAttribute("version", dependency.Version));
+ if (dependency.Exclude is not null)
+ {
+ element.Add(new XAttribute("exclude", dependency.Exclude));
+ }
+ return element;
+ });
+ var nuspec = new XDocument(
+ new XElement(
+ "package",
+ new XElement(
+ "metadata",
+ new XElement("id", id),
+ new XElement("version", version),
+ new XElement("authors", "WindowTranslator.Tests"),
+ new XElement("description", "Test package"),
+ new XElement(
+ "dependencies",
+ new XElement(
+ "group",
+ new XAttribute("targetFramework", "net10.0"),
+ dependencyElements)))));
+ var nuspecEntry = archive.CreateEntry($"{id}.nuspec");
+ using (var nuspecStream = nuspecEntry.Open())
+ {
+ nuspec.Save(nuspecStream);
+ }
+
+ foreach (var (path, content) in entries)
+ {
+ var entry = archive.CreateEntry(path);
+ using var entryStream = entry.Open();
+ entryStream.Write(content);
+ }
+ }
+
+ return stream.ToArray();
+ }
+
+ private static string CreateTestDirectory()
+ {
+ var path = Path.Combine(
+ Path.GetTempPath(),
+ "WindowTranslator.Tests",
+ Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(path);
+ return path;
+ }
+
+ private static void DeleteTestDirectory(string path)
+ {
+ try
+ {
+ if (Directory.Exists(path))
+ {
+ Directory.Delete(path, recursive: true);
+ }
+ }
+ catch
+ {
+ // テスト用ディレクトリの後始末はテスト結果へ影響させない
+ }
+ }
+
+ private sealed record TestDependency(string Id, string Version, string? Exclude = null);
+
+ private sealed class InMemoryNuGetHandler : HttpMessageHandler
+ {
+ private readonly Dictionary<(string Id, string Version), byte[]> packages = new();
+
+ public List RequestedPaths { get; } = [];
+
+ public void AddPackage(string id, string version, byte[] package)
+ => this.packages[(id.ToLowerInvariant(), version.ToLowerInvariant())] = package;
+
+ protected override Task SendAsync(
+ HttpRequestMessage request,
+ CancellationToken cancellationToken)
+ {
+ var path = request.RequestUri!.AbsolutePath;
+ this.RequestedPaths.Add(path);
+ var segments = path.Trim('/').Split('/');
+ if (segments.Length == 3
+ && segments[0].Equals("v3-flatcontainer", StringComparison.OrdinalIgnoreCase)
+ && segments[2].Equals("index.json", StringComparison.OrdinalIgnoreCase))
+ {
+ var id = segments[1].ToLowerInvariant();
+ var versions = this.packages.Keys
+ .Where(key => key.Id == id)
+ .Select(key => key.Version)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(version => version, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(
+ JsonSerializer.Serialize(new { versions }),
+ Encoding.UTF8,
+ "application/json"),
+ });
+ }
+
+ if (segments.Length == 4
+ && segments[0].Equals("v3-flatcontainer", StringComparison.OrdinalIgnoreCase))
+ {
+ var key = (segments[1].ToLowerInvariant(), segments[2].ToLowerInvariant());
+ if (this.packages.TryGetValue(key, out var package))
+ {
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new ByteArrayContent(package),
+ });
+ }
+ }
+
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
+ }
+ }
+}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
new file mode 100644
index 00000000..51822ef2
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
@@ -0,0 +1,594 @@
+using System.IO;
+using System.IO.Compression;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Xml.Linq;
+using Microsoft.Extensions.Logging;
+using NuGet.Versioning;
+
+namespace WindowTranslator.Modules.PluginStore;
+
+///
+/// NuGetパッケージとそのランタイム依存関係を、プラグインフォルダへ展開します。
+///
+internal sealed class NuGetPackageInstaller(HttpClient httpClient, ILogger logger)
+{
+ private const string FlatContainerBase = "https://api.nuget.org/v3-flatcontainer";
+
+ private static readonly string[] CompatibleFrameworks =
+ [
+ "net10.0-windows",
+ "net10.0",
+ "net9.0-windows",
+ "net9.0",
+ "net8.0-windows",
+ "net8.0",
+ "net7.0-windows",
+ "net7.0",
+ "net6.0-windows",
+ "net6.0",
+ "net5.0-windows",
+ "net5.0",
+ "netcoreapp3.1",
+ "netstandard2.1",
+ "netstandard2.0",
+ ];
+
+ private static readonly string[] CompatibleRuntimeIdentifiers = ["win-x64", "win", "any"];
+
+ private readonly HttpClient httpClient = httpClient;
+ private readonly ILogger logger = logger;
+
+ public async Task InstallAsync(
+ string packageId,
+ string version,
+ string destinationDirectory,
+ IProgress? progress,
+ CancellationToken cancellationToken)
+ {
+ ValidatePackageId(packageId);
+ var requestedVersion = NuGetVersion.Parse(version);
+ var workDirectory = Path.Combine(
+ Path.GetTempPath(),
+ "WindowTranslatorPlugins",
+ Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(workDirectory);
+
+ try
+ {
+ var artifacts = await ResolvePackageGraphAsync(
+ packageId,
+ requestedVersion,
+ workDirectory,
+ progress,
+ cancellationToken).ConfigureAwait(false);
+
+ Directory.CreateDirectory(destinationDirectory);
+ foreach (var artifact in artifacts.OrderByDescending(a =>
+ a.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)))
+ {
+ ExtractPackageAssets(
+ artifact.PackagePath,
+ destinationDirectory,
+ requirePluginAssembly: artifact.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+ finally
+ {
+ TryDeleteDirectory(workDirectory);
+ }
+ }
+
+ internal static string? SelectBestTfm(IEnumerable frameworks)
+ {
+ var candidates = frameworks
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Select(original => (Original: original, Normalized: NormalizeFramework(original)))
+ .ToArray();
+
+ foreach (var compatibleFramework in CompatibleFrameworks)
+ {
+ var match = candidates
+ .OrderByDescending(c => c.Normalized, StringComparer.OrdinalIgnoreCase)
+ .FirstOrDefault(c => compatibleFramework.EndsWith("-windows", StringComparison.Ordinal)
+ ? c.Normalized.StartsWith(compatibleFramework, StringComparison.OrdinalIgnoreCase)
+ : c.Normalized.Equals(compatibleFramework, StringComparison.OrdinalIgnoreCase));
+ if (match.Original is not null)
+ {
+ return match.Original;
+ }
+ }
+
+ return null;
+ }
+
+ private async Task> ResolvePackageGraphAsync(
+ string rootPackageId,
+ NuGetVersion rootVersion,
+ string workDirectory,
+ IProgress? progress,
+ CancellationToken cancellationToken)
+ {
+ var constraints = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ var selectedVersions = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var queue = new Queue();
+ var queued = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ AddConstraint(
+ rootPackageId,
+ "$root",
+ new VersionRange(
+ rootVersion,
+ includeMinVersion: true,
+ maxVersion: rootVersion,
+ includeMaxVersion: true));
+
+ while (queue.Count > 0)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var currentId = queue.Dequeue();
+ queued.Remove(currentId);
+
+ if (!constraints.TryGetValue(currentId, out var currentConstraints)
+ || currentConstraints.Count == 0)
+ {
+ if (selectedVersions.Remove(currentId))
+ {
+ artifacts.Remove(currentId);
+ RemoveConstraintsFrom(currentId);
+ }
+ continue;
+ }
+
+ var ranges = currentConstraints.Select(c => c.Range).ToArray();
+ var resolvedVersion = currentId.Equals(rootPackageId, StringComparison.OrdinalIgnoreCase)
+ ? rootVersion
+ : await ResolveDependencyVersionAsync(currentId, ranges, cancellationToken).ConfigureAwait(false);
+
+ if (!ranges.All(r => r.Satisfies(resolvedVersion)))
+ {
+ throw new InvalidOperationException(
+ $"パッケージ {currentId} の依存バージョンを解決できませんでした: {string.Join(", ", ranges)}");
+ }
+
+ if (selectedVersions.TryGetValue(currentId, out var existingVersion)
+ && existingVersion == resolvedVersion)
+ {
+ continue;
+ }
+
+ if (selectedVersions.ContainsKey(currentId))
+ {
+ RemoveConstraintsFrom(currentId);
+ }
+ selectedVersions[currentId] = resolvedVersion;
+ var packagePath = Path.Combine(
+ workDirectory,
+ $"{currentId.ToLowerInvariant()}.{resolvedVersion.ToNormalizedString().ToLowerInvariant()}.nupkg");
+ await DownloadPackageAsync(
+ currentId,
+ resolvedVersion,
+ packagePath,
+ currentId.Equals(rootPackageId, StringComparison.OrdinalIgnoreCase) ? progress : null,
+ cancellationToken).ConfigureAwait(false);
+
+ artifacts[currentId] = new PackageArtifact(currentId, resolvedVersion, packagePath);
+ foreach (var dependency in ReadRuntimeDependencies(packagePath))
+ {
+ AddConstraint(dependency.Id, currentId, dependency.VersionRange);
+ }
+ }
+
+ return artifacts.Values.ToArray();
+
+ void AddConstraint(string id, string source, VersionRange range)
+ {
+ ValidatePackageId(id);
+ if (!constraints.TryGetValue(id, out var packageConstraints))
+ {
+ packageConstraints = [];
+ constraints[id] = packageConstraints;
+ }
+
+ if (packageConstraints.Any(c =>
+ c.Source.Equals(source, StringComparison.OrdinalIgnoreCase)
+ && c.Range.ToString().Equals(range.ToString(), StringComparison.OrdinalIgnoreCase)))
+ {
+ return;
+ }
+
+ packageConstraints.Add(new DependencyConstraint(source, range));
+ Enqueue(id);
+ }
+
+ void RemoveConstraintsFrom(string source)
+ {
+ foreach (var (id, packageConstraints) in constraints)
+ {
+ if (packageConstraints.RemoveAll(c =>
+ c.Source.Equals(source, StringComparison.OrdinalIgnoreCase)) > 0)
+ {
+ Enqueue(id);
+ }
+ }
+ }
+
+ void Enqueue(string id)
+ {
+ if (queued.Add(id))
+ {
+ queue.Enqueue(id);
+ }
+ }
+ }
+
+ private async Task ResolveDependencyVersionAsync(
+ string packageId,
+ IReadOnlyCollection ranges,
+ CancellationToken cancellationToken)
+ {
+ var versionsUrl = $"{FlatContainerBase}/{packageId.ToLowerInvariant()}/index.json";
+ using var response = await this.httpClient.GetAsync(versionsUrl, cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+ await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ var versionList = await JsonSerializer.DeserializeAsync(
+ content,
+ cancellationToken: cancellationToken).ConfigureAwait(false);
+
+ var compatibleVersions = versionList?.Versions?
+ .Select(NuGetVersion.Parse)
+ .Where(v => ranges.All(r => r.Satisfies(v)))
+ .OrderBy(v => v)
+ .ToArray() ?? [];
+ return compatibleVersions.FirstOrDefault(v => !v.IsPrerelease)
+ ?? compatibleVersions.FirstOrDefault()
+ ?? throw new InvalidOperationException(
+ $"パッケージ {packageId} の依存条件を満たすバージョンがありません: {string.Join(", ", ranges)}");
+ }
+
+ private async Task DownloadPackageAsync(
+ string packageId,
+ NuGetVersion version,
+ string destinationPath,
+ IProgress? progress,
+ CancellationToken cancellationToken)
+ {
+ var packageIdLower = packageId.ToLowerInvariant();
+ var versionLower = version.ToNormalizedString().ToLowerInvariant();
+ var url = $"{FlatContainerBase}/{packageIdLower}/{versionLower}/{packageIdLower}.{versionLower}.nupkg";
+ this.logger.LogInformation("NuGetパッケージをダウンロード中: {PackageId} {Version}", packageId, version);
+
+ using var response = await this.httpClient.GetAsync(
+ url,
+ HttpCompletionOption.ResponseHeadersRead,
+ cancellationToken).ConfigureAwait(false);
+ response.EnsureSuccessStatusCode();
+
+ var totalBytes = response.Content.Headers.ContentLength ?? -1;
+ var downloadedBytes = 0L;
+ await using var source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using var destination = File.Create(destinationPath);
+ var buffer = new byte[81920];
+ int bytesRead;
+ while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0)
+ {
+ await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
+ downloadedBytes += bytesRead;
+ if (totalBytes > 0)
+ {
+ progress?.Report((double)downloadedBytes / totalBytes);
+ }
+ }
+ }
+
+ private static List ReadRuntimeDependencies(string packagePath)
+ {
+ using var archive = ZipFile.OpenRead(packagePath);
+ var nuspecEntry = archive.Entries.FirstOrDefault(e =>
+ e.FullName.EndsWith(".nuspec", StringComparison.OrdinalIgnoreCase))
+ ?? throw new InvalidOperationException("パッケージにnuspecが見つかりませんでした。");
+
+ using var stream = nuspecEntry.Open();
+ var document = XDocument.Load(stream);
+ var metadata = document.Root?.Elements().FirstOrDefault(e => e.Name.LocalName == "metadata")
+ ?? throw new InvalidOperationException("nuspecのmetadataが見つかりませんでした。");
+ var dependencies = metadata.Elements().FirstOrDefault(e => e.Name.LocalName == "dependencies");
+ if (dependencies is null)
+ {
+ return [];
+ }
+
+ var result = new List();
+ result.AddRange(ParseDependencyElements(
+ dependencies.Elements().Where(e => e.Name.LocalName == "dependency")));
+
+ var groups = dependencies.Elements()
+ .Where(e => e.Name.LocalName == "group")
+ .Select(e => (
+ Element: e,
+ Framework: e.Attribute("targetFramework")?.Value))
+ .ToArray();
+ if (groups.Length == 0)
+ {
+ return result;
+ }
+
+ var frameworkGroups = groups.Where(g =>
+ !string.IsNullOrWhiteSpace(g.Framework)
+ && !g.Framework.Equals("any", StringComparison.OrdinalIgnoreCase)).ToArray();
+ if (frameworkGroups.Length > 0)
+ {
+ var selectedFramework = SelectBestTfm(frameworkGroups.Select(g => g.Framework!));
+ if (selectedFramework is not null)
+ {
+ result.AddRange(ParseDependencyElements(
+ frameworkGroups.First(g => string.Equals(
+ g.Framework,
+ selectedFramework,
+ StringComparison.OrdinalIgnoreCase)).Element.Elements()));
+ return result;
+ }
+ }
+
+ var fallbackGroup = groups.FirstOrDefault(g =>
+ string.IsNullOrWhiteSpace(g.Framework)
+ || g.Framework.Equals("any", StringComparison.OrdinalIgnoreCase));
+ if (fallbackGroup.Element is not null)
+ {
+ result.AddRange(ParseDependencyElements(fallbackGroup.Element.Elements()));
+ return result;
+ }
+
+ throw new InvalidOperationException("互換性のある依存関係グループが見つかりませんでした。");
+ }
+
+ private static IEnumerable ParseDependencyElements(IEnumerable elements)
+ {
+ foreach (var element in elements.Where(e => e.Name.LocalName == "dependency"))
+ {
+ var id = element.Attribute("id")?.Value;
+ if (string.IsNullOrWhiteSpace(id) || !IncludesRuntimeAssets(element))
+ {
+ continue;
+ }
+
+ var versionText = element.Attribute("version")?.Value;
+ yield return new PackageDependency(
+ id,
+ string.IsNullOrWhiteSpace(versionText) ? VersionRange.All : VersionRange.Parse(versionText));
+ }
+ }
+
+ private static bool IncludesRuntimeAssets(XElement dependency)
+ {
+ var excluded = SplitAssets(dependency.Attribute("exclude")?.Value);
+ if (excluded.Contains("all") || excluded.Contains("runtime"))
+ {
+ return false;
+ }
+
+ var included = SplitAssets(dependency.Attribute("include")?.Value);
+ return included.Count == 0 || included.Contains("all") || included.Contains("runtime");
+ }
+
+ private static HashSet SplitAssets(string? assets)
+ => string.IsNullOrWhiteSpace(assets)
+ ? new(StringComparer.OrdinalIgnoreCase)
+ : assets.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ private static void ExtractPackageAssets(
+ string packagePath,
+ string destinationDirectory,
+ bool requirePluginAssembly)
+ {
+ using var archive = ZipFile.OpenRead(packagePath);
+ var libEntries = archive.Entries
+ .Where(e => e.FullName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(e.Name)
+ && e.Name != "_._")
+ .ToArray();
+ var libGroups = libEntries
+ .Where(e => e.FullName.Split('/').Length >= 3)
+ .GroupBy(e => e.FullName.Split('/')[1])
+ .ToArray();
+ var hasPluginAssembly = false;
+
+ if (libGroups.Length > 0)
+ {
+ var selectedFramework = SelectBestTfm(libGroups.Select(g => g.Key));
+ if (selectedFramework is not null)
+ {
+ var selectedEntries = libGroups.First(g => g.Key.Equals(
+ selectedFramework,
+ StringComparison.OrdinalIgnoreCase)).ToArray();
+ hasPluginAssembly = selectedEntries.Any(e =>
+ e.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase));
+
+ ExtractEntries(
+ selectedEntries,
+ $"lib/{selectedFramework}/",
+ destinationDirectory);
+ }
+ }
+
+ hasPluginAssembly |= ExtractRuntimeAssets(archive, destinationDirectory);
+ if (requirePluginAssembly && !hasPluginAssembly)
+ {
+ throw new InvalidOperationException("プラグインパッケージに互換性のあるアセンブリが見つかりませんでした。");
+ }
+ }
+
+ private static bool ExtractRuntimeAssets(ZipArchive archive, string destinationDirectory)
+ {
+ var hasManagedAssembly = false;
+ var runtimeLibIdentifier = CompatibleRuntimeIdentifiers.FirstOrDefault(rid =>
+ archive.Entries.Any(e => e.FullName.StartsWith(
+ $"runtimes/{rid}/lib/",
+ StringComparison.OrdinalIgnoreCase)));
+ var runtimeLibPrefix = $"runtimes/{runtimeLibIdentifier}/lib/";
+ var runtimeLibGroups = archive.Entries
+ .Where(e => runtimeLibIdentifier is not null
+ && e.FullName.StartsWith(runtimeLibPrefix, StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(e.Name))
+ .Where(e => e.FullName.Split('/').Length >= 5)
+ .GroupBy(e => e.FullName.Split('/')[3])
+ .ToArray();
+ if (runtimeLibGroups.Length > 0)
+ {
+ var selectedFramework = SelectBestTfm(runtimeLibGroups.Select(g => g.Key));
+ if (selectedFramework is not null)
+ {
+ var selectedEntries = runtimeLibGroups.First(g => g.Key.Equals(
+ selectedFramework,
+ StringComparison.OrdinalIgnoreCase)).ToArray();
+ hasManagedAssembly = selectedEntries.Any(e =>
+ e.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase));
+ ExtractEntries(
+ selectedEntries,
+ string.Empty,
+ destinationDirectory);
+ }
+ }
+
+ var nativeRuntimeIdentifier = CompatibleRuntimeIdentifiers.FirstOrDefault(rid =>
+ archive.Entries.Any(e => e.FullName.StartsWith(
+ $"runtimes/{rid}/native/",
+ StringComparison.OrdinalIgnoreCase)));
+ if (nativeRuntimeIdentifier is null)
+ {
+ return hasManagedAssembly;
+ }
+
+ var nativePrefix = $"runtimes/{nativeRuntimeIdentifier}/native/";
+ ExtractEntries(
+ archive.Entries.Where(e => e.FullName.StartsWith(nativePrefix, StringComparison.OrdinalIgnoreCase)
+ && !string.IsNullOrEmpty(e.Name)),
+ string.Empty,
+ destinationDirectory);
+ return hasManagedAssembly;
+ }
+
+ private static void ExtractEntries(
+ IEnumerable entries,
+ string prefix,
+ string destinationDirectory)
+ {
+ foreach (var entry in entries)
+ {
+ var relativePath = entry.FullName[prefix.Length..];
+ if (string.IsNullOrWhiteSpace(relativePath))
+ {
+ continue;
+ }
+
+ var destinationPath = GetSafeDestinationPath(destinationDirectory, relativePath);
+ Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
+ if (File.Exists(destinationPath))
+ {
+ using var existing = File.OpenRead(destinationPath);
+ using var incoming = entry.Open();
+ if (!StreamsEqual(existing, incoming))
+ {
+ throw new InvalidOperationException(
+ $"依存パッケージ間でファイルが競合しています: {relativePath}");
+ }
+ continue;
+ }
+
+ entry.ExtractToFile(destinationPath);
+ }
+ }
+
+ private static string GetSafeDestinationPath(string destinationDirectory, string relativePath)
+ {
+ var root = Path.GetFullPath(destinationDirectory)
+ .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ + Path.DirectorySeparatorChar;
+ var destination = Path.GetFullPath(Path.Combine(
+ root,
+ relativePath.Replace('/', Path.DirectorySeparatorChar)));
+ if (!destination.StartsWith(root, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException($"不正なパッケージエントリです: {relativePath}");
+ }
+
+ return destination;
+ }
+
+ private static bool StreamsEqual(Stream left, Stream right)
+ {
+ if (left.Length != right.Length)
+ {
+ return false;
+ }
+
+ var leftBuffer = new byte[81920];
+ var rightBuffer = new byte[81920];
+ int leftRead;
+ while ((leftRead = left.Read(leftBuffer, 0, leftBuffer.Length)) > 0)
+ {
+ var rightRead = right.Read(rightBuffer, 0, rightBuffer.Length);
+ if (leftRead != rightRead
+ || !leftBuffer.AsSpan(0, leftRead).SequenceEqual(rightBuffer.AsSpan(0, rightRead)))
+ {
+ return false;
+ }
+ }
+
+ return right.ReadByte() == -1;
+ }
+
+ private static string NormalizeFramework(string framework)
+ {
+ var normalized = framework.Replace(" ", string.Empty, StringComparison.Ordinal);
+ const string netCoreAppPrefix = ".NETCoreApp,Version=v";
+ const string netStandardPrefix = ".NETStandard,Version=v";
+ if (normalized.StartsWith(netCoreAppPrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return $"net{normalized[netCoreAppPrefix.Length..]}";
+ }
+ if (normalized.StartsWith(netStandardPrefix, StringComparison.OrdinalIgnoreCase))
+ {
+ return $"netstandard{normalized[netStandardPrefix.Length..]}";
+ }
+ return normalized;
+ }
+
+ private static void ValidatePackageId(string packageId)
+ {
+ if (string.IsNullOrWhiteSpace(packageId)
+ || packageId.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0
+ || packageId.Contains(Path.DirectorySeparatorChar)
+ || packageId.Contains(Path.AltDirectorySeparatorChar))
+ {
+ throw new InvalidOperationException($"不正なNuGetパッケージIDです: {packageId}");
+ }
+ }
+
+ private static void TryDeleteDirectory(string directory)
+ {
+ try
+ {
+ if (Directory.Exists(directory))
+ {
+ Directory.Delete(directory, recursive: true);
+ }
+ }
+ catch
+ {
+ // 一時ディレクトリの後始末失敗はインストール結果へ影響させない
+ }
+ }
+
+ private sealed record PackageArtifact(string Id, NuGetVersion Version, string PackagePath);
+
+ private sealed record PackageDependency(string Id, VersionRange VersionRange);
+
+ private sealed record DependencyConstraint(string Source, VersionRange Range);
+
+ private sealed record VersionIndex(
+ [property: JsonPropertyName("versions")] string[]? Versions);
+}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index f961c801..9126a1c6 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -10,16 +10,23 @@ namespace WindowTranslator.Modules.PluginStore;
///
public sealed class NuGetPluginCatalog : IPluginCatalog
{
- private static readonly string TempDir =
+ private static readonly string DefaultTempDir =
Path.Combine(Path.GetTempPath(), "WindowTranslator", "plugins");
private readonly string sourceDir;
+ private readonly string tempDir;
private readonly FolderPluginCatalog innerCatalog;
public NuGetPluginCatalog(string sourceDir, FolderPluginCatalogOptions options)
+ : this(sourceDir, DefaultTempDir, options)
+ {
+ }
+
+ internal NuGetPluginCatalog(string sourceDir, string tempDir, FolderPluginCatalogOptions options)
{
this.sourceDir = sourceDir;
- this.innerCatalog = new FolderPluginCatalog(TempDir, options);
+ this.tempDir = tempDir;
+ this.innerCatalog = new FolderPluginCatalog(tempDir, options);
}
///
@@ -29,20 +36,15 @@ public NuGetPluginCatalog(string sourceDir, FolderPluginCatalogOptions options)
public async Task Initialize()
{
// ロック解除のために一時フォルダを削除してからコピー
- if (Directory.Exists(TempDir))
+ if (Directory.Exists(this.tempDir))
{
- Directory.Delete(TempDir, recursive: true);
+ Directory.Delete(this.tempDir, recursive: true);
}
- Directory.CreateDirectory(TempDir);
+ Directory.CreateDirectory(this.tempDir);
if (Directory.Exists(this.sourceDir))
{
- // プラグインのサブフォルダのみコピー(nuget-manifest.json等のファイルはスキップ)
- foreach (var subDir in Directory.GetDirectories(this.sourceDir))
- {
- var destSubDir = Path.Combine(TempDir, Path.GetFileName(subDir));
- CopyDirectory(subDir, destSubDir);
- }
+ CopyPluginFiles(this.sourceDir, this.tempDir);
}
await this.innerCatalog.Initialize().ConfigureAwait(false);
@@ -54,18 +56,58 @@ public async Task Initialize()
///
public Plugin Get(string name, Version version) => this.innerCatalog.Get(name, version);
- private static void CopyDirectory(string source, string destination)
+ internal static void CopyPluginFiles(string source, string destination)
{
Directory.CreateDirectory(destination);
+
foreach (var file in Directory.GetFiles(source))
{
- var destinationFile = Path.Combine(destination, Path.GetFileName(file));
- if (File.Exists(destinationFile))
+ var fileName = Path.GetFileName(file);
+ if (IsManagementFile(fileName))
{
continue;
}
- File.Copy(file, destinationFile);
+ var destinationFile = Path.Combine(destination, fileName);
+ if (!File.Exists(destinationFile))
+ {
+ File.Copy(file, destinationFile);
+ }
+ }
+
+ foreach (var subDir in Directory.GetDirectories(source))
+ {
+ var directoryName = Path.GetFileName(subDir);
+ if (IsWorkingDirectory(directoryName))
+ {
+ continue;
+ }
+
+ CopyDirectory(subDir, Path.Combine(destination, directoryName));
+ }
+ }
+
+ private static bool IsWorkingDirectory(string directoryName)
+ => directoryName.EndsWith(".backup", StringComparison.OrdinalIgnoreCase)
+ || directoryName.Contains(".backup-", StringComparison.OrdinalIgnoreCase)
+ || directoryName.Contains(".installing-", StringComparison.OrdinalIgnoreCase);
+
+ private static bool IsManagementFile(string fileName)
+ => fileName.Equals("nuget-manifest.json", StringComparison.OrdinalIgnoreCase)
+ || fileName.StartsWith("nuget-manifest.json.tmp-", StringComparison.OrdinalIgnoreCase)
+ || fileName.EndsWith(".pending-delete", StringComparison.OrdinalIgnoreCase)
+ || fileName.Contains(".pending-delete.tmp-", StringComparison.OrdinalIgnoreCase);
+
+ private static void CopyDirectory(string source, string destination)
+ {
+ Directory.CreateDirectory(destination);
+ foreach (var file in Directory.GetFiles(source))
+ {
+ var destinationFile = Path.Combine(destination, Path.GetFileName(file));
+ if (!File.Exists(destinationFile))
+ {
+ File.Copy(file, destinationFile);
+ }
}
foreach (var subDir in Directory.GetDirectories(source))
{
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 552ab054..80044091 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -1,9 +1,9 @@
using System.IO;
-using System.IO.Compression;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
+using NuGet.Versioning;
namespace WindowTranslator.Modules.PluginStore;
@@ -50,9 +50,6 @@ public sealed class NuGetPluginService : IDisposable
["TesseractOcr"] = "WindowTranslator.Plugin.TesseractOCRPlugin",
};
- private static readonly string UserPluginsDir = Path.Combine(PathUtility.UserDir, "plugins");
- private static readonly string ManifestPath = Path.Combine(UserPluginsDir, "nuget-manifest.json");
-
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
@@ -63,15 +60,32 @@ public sealed class NuGetPluginService : IDisposable
private readonly HttpClient httpClient;
private readonly ILogger logger;
+ private readonly string userPluginsDir;
+ private readonly string manifestPath;
+ private readonly bool ownsHttpClient;
+ private readonly SemaphoreSlim operationLock = new(1, 1);
private string? searchUrl;
public NuGetPluginService(ILogger logger)
+ : this(
+ logger,
+ new HttpClient { Timeout = TimeSpan.FromSeconds(30) },
+ Path.Combine(PathUtility.UserDir, "plugins"),
+ ownsHttpClient: true)
+ {
+ }
+
+ internal NuGetPluginService(
+ ILogger logger,
+ HttpClient httpClient,
+ string userPluginsDir,
+ bool ownsHttpClient = false)
{
- this.httpClient = new HttpClient
- {
- Timeout = TimeSpan.FromSeconds(30),
- };
this.logger = logger;
+ this.httpClient = httpClient;
+ this.userPluginsDir = Path.GetFullPath(userPluginsDir);
+ this.manifestPath = Path.Combine(this.userPluginsDir, "nuget-manifest.json");
+ this.ownsHttpClient = ownsHttpClient;
}
///
@@ -80,13 +94,22 @@ public NuGetPluginService(ILogger logger)
public async Task InstallLatestPackageAsync(string packageId, IProgress? progress = null, CancellationToken cancellationToken = default)
{
var versionsUrl = $"{NuGetFlatContainerBase}/{packageId.ToLowerInvariant()}/index.json";
- var response = await this.httpClient.GetAsync(versionsUrl, cancellationToken).ConfigureAwait(false);
+ using var response = await this.httpClient.GetAsync(versionsUrl, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var versions = await JsonSerializer.DeserializeAsync(content, JsonOptions, cancellationToken).ConfigureAwait(false);
- var latestVersion = versions?.Versions?.LastOrDefault()
+ var parsedVersions = versions?.Versions?
+ .Select(NuGetVersion.Parse)
+ .OrderByDescending(v => v)
+ .ToArray() ?? [];
+ var latestVersion = parsedVersions.FirstOrDefault(v => !v.IsPrerelease)
+ ?? parsedVersions.FirstOrDefault()
?? throw new InvalidOperationException($"パッケージ {packageId} のバージョン一覧を取得できませんでした。");
- await InstallPackageAsync(packageId, latestVersion, progress, cancellationToken).ConfigureAwait(false);
+ await InstallPackageAsync(
+ packageId,
+ latestVersion.ToNormalizedString(),
+ progress,
+ cancellationToken).ConfigureAwait(false);
}
///
@@ -151,12 +174,36 @@ public async Task AutoInstallFromSettingsAsync(string settingsPath, Cancellation
{
await InstallLatestPackageAsync(packageId, cancellationToken: cancellationToken).ConfigureAwait(false);
}
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
+ catch (HttpRequestException ex)
+ {
+ this.logger.LogWarning(
+ ex,
+ "NuGetへ接続できないため、プラグインの自動インストールを中断します: {PackageId}",
+ packageId);
+ break;
+ }
+ catch (TaskCanceledException ex)
+ {
+ this.logger.LogWarning(
+ ex,
+ "NuGet接続がタイムアウトしたため、プラグインの自動インストールを中断します: {PackageId}",
+ packageId);
+ break;
+ }
catch (Exception ex)
{
this.logger.LogWarning(ex, "プラグイン {PackageId} の自動インストールに失敗しました。", packageId);
}
}
}
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ throw;
+ }
catch (Exception ex)
{
this.logger.LogWarning(ex, "設定からのプラグイン自動インストール処理中にエラーが発生しました。");
@@ -176,10 +223,10 @@ public async Task> SearchPackagesAsync(Cancellat
var url = $"{this.searchUrl}?q=tags:{PluginTag}&take=100&semVerLevel=2.0.0&prerelease=false";
this.logger.LogDebug("NuGet検索URL: {Url}", url);
- var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
+ using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var result = await JsonSerializer.DeserializeAsync(content, JsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("NuGet検索結果のデシリアライズに失敗しました。");
@@ -201,60 +248,100 @@ public async Task> SearchPackagesAsync(Cancellat
///
public async Task InstallPackageAsync(string packageId, string version, IProgress? progress = null, CancellationToken cancellationToken = default)
{
- var packageIdLower = packageId.ToLowerInvariant();
- var versionLower = version.ToLowerInvariant();
- var nupkgUrl = $"{NuGetFlatContainerBase}/{packageIdLower}/{versionLower}/{packageIdLower}.{versionLower}.nupkg";
-
- this.logger.LogInformation("パッケージをダウンロード中: {PackageId} {Version}", packageId, version);
-
- // 一時ディレクトリにダウンロード
- var tempDir = Path.Combine(Path.GetTempPath(), "WindowTranslatorPlugins", packageId);
- Directory.CreateDirectory(tempDir);
- var tempNupkgPath = Path.Combine(tempDir, $"{packageIdLower}.{versionLower}.nupkg");
-
+ var operationId = Guid.NewGuid().ToString("N");
+ var targetDir = GetPackageDirectory(packageId);
+ var stagingDir = Path.Combine(this.userPluginsDir, $".{packageId}.installing-{operationId}");
+ var backupDir = $"{targetDir}.backup-{operationId}";
+ var pendingDeleteMarker = GetPendingDeleteMarker(packageId);
+ var markerWasPresent = false;
+ string? markerContent = null;
+ var targetMoved = false;
+ var stagingMoved = false;
+ await this.operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
- await DownloadFileAsync(nupkgUrl, tempNupkgPath, progress, cancellationToken).ConfigureAwait(false);
+ markerWasPresent = File.Exists(pendingDeleteMarker);
+ markerContent = markerWasPresent
+ ? await File.ReadAllTextAsync(pendingDeleteMarker, cancellationToken).ConfigureAwait(false)
+ : null;
+ Directory.CreateDirectory(this.userPluginsDir);
+ var installer = new NuGetPackageInstaller(this.httpClient, this.logger);
+ await installer.InstallAsync(
+ packageId,
+ version,
+ stagingDir,
+ progress,
+ cancellationToken).ConfigureAwait(false);
+
+ var currentManifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ var updatedManifest = AddOrUpdatePackage(currentManifest, packageId, version);
+
+ if (markerWasPresent)
+ {
+ File.Delete(pendingDeleteMarker);
+ }
- // ターゲットディレクトリを準備
- var targetDir = Path.Combine(UserPluginsDir, packageId);
- // 古いファイルをバックアップして削除する前に一時フォルダへ移動
- var backupDir = $"{targetDir}.backup";
if (Directory.Exists(targetDir))
{
- if (Directory.Exists(backupDir))
- Directory.Delete(backupDir, recursive: true);
Directory.Move(targetDir, backupDir);
+ targetMoved = true;
}
- Directory.CreateDirectory(targetDir);
+ Directory.Move(stagingDir, targetDir);
+ stagingMoved = true;
+ await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
try
{
- // nupkgを展開して必要なDLLをコピー
- ExtractPluginDlls(tempNupkgPath, targetDir);
- this.logger.LogInformation("パッケージの展開完了: {PackageId} -> {TargetDir}", packageId, targetDir);
+ if (Directory.Exists(backupDir))
+ {
+ Directory.Delete(backupDir, recursive: true);
+ }
}
- catch
+ catch (Exception ex)
{
- // 失敗したら元に戻す
- Directory.Delete(targetDir, recursive: true);
- if (Directory.Exists(backupDir))
- Directory.Move(backupDir, targetDir);
- throw;
+ this.logger.LogWarning(ex, "プラグインバックアップの削除に失敗しました: {BackupDir}", backupDir);
}
- // バックアップを削除
- if (Directory.Exists(backupDir))
- Directory.Delete(backupDir, recursive: true);
-
- // マニフェストを更新
- await UpdateManifestAsync(packageId, version, cancellationToken).ConfigureAwait(false);
+ this.logger.LogInformation(
+ "パッケージのインストール完了: {PackageId} {Version} -> {TargetDir}",
+ packageId,
+ version,
+ targetDir);
+ }
+ catch
+ {
+ try
+ {
+ if (stagingMoved && Directory.Exists(targetDir))
+ {
+ Directory.Move(targetDir, stagingDir);
+ }
+ if (targetMoved && Directory.Exists(backupDir))
+ {
+ Directory.Move(backupDir, targetDir);
+ }
+ if (markerWasPresent && !File.Exists(pendingDeleteMarker))
+ {
+ await File.WriteAllTextAsync(
+ pendingDeleteMarker,
+ markerContent ?? packageId,
+ CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+ catch (Exception rollbackException)
+ {
+ this.logger.LogError(
+ rollbackException,
+ "プラグイン {PackageId} のインストール失敗後の復旧に失敗しました。",
+ packageId);
+ }
+ throw;
}
finally
{
- // 一時ファイルを削除
- try { File.Delete(tempNupkgPath); } catch { /* ignore */ }
+ TryDeleteDirectory(stagingDir);
+ this.operationLock.Release();
}
}
@@ -263,17 +350,43 @@ public async Task InstallPackageAsync(string packageId, string version, IProgres
///
public async Task UninstallPackageAsync(string packageId, CancellationToken cancellationToken = default)
{
- this.logger.LogInformation("パッケージをアンインストール: {PackageId}", packageId);
+ await this.operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ this.logger.LogInformation("パッケージをアンインストール: {PackageId}", packageId);
+ _ = GetPackageDirectory(packageId);
+ Directory.CreateDirectory(this.userPluginsDir);
- var targetDir = Path.Combine(UserPluginsDir, packageId);
- // 実行中のDLLがロックされている可能性があるため、削除マーカーを置く
- var pendingDeleteMarker = Path.Combine(UserPluginsDir, $"{packageId}.pending-delete");
- await File.WriteAllTextAsync(pendingDeleteMarker, packageId, cancellationToken).ConfigureAwait(false);
+ var pendingDeleteMarker = GetPendingDeleteMarker(packageId);
+ var markerAlreadyExisted = File.Exists(pendingDeleteMarker);
+ var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ var updatedManifest = RemovePackage(manifest, packageId);
- // マニフェストから削除
- await RemoveFromManifestAsync(packageId, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ await WriteTextAtomicallyAsync(
+ pendingDeleteMarker,
+ packageId,
+ cancellationToken).ConfigureAwait(false);
+ await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
+ }
+ catch
+ {
+ if (!markerAlreadyExisted)
+ {
+ TryDeleteFile(pendingDeleteMarker);
+ }
+ throw;
+ }
- this.logger.LogInformation("パッケージ {PackageId} をアンインストールキューに追加しました。再起動後に完全に削除されます。", packageId);
+ this.logger.LogInformation(
+ "パッケージ {PackageId} をアンインストールキューに追加しました。再起動後に完全に削除されます。",
+ packageId);
+ }
+ finally
+ {
+ this.operationLock.Release();
+ }
}
///
@@ -281,27 +394,43 @@ public async Task UninstallPackageAsync(string packageId, CancellationToken canc
///
public void ProcessPendingDeletions()
{
- if (!Directory.Exists(UserPluginsDir))
- return;
-
- foreach (var markerFile in Directory.GetFiles(UserPluginsDir, "*.pending-delete"))
+ this.operationLock.Wait();
+ try
{
- try
+ if (!Directory.Exists(this.userPluginsDir))
{
- var packageId = File.ReadAllText(markerFile);
- var targetDir = Path.Combine(UserPluginsDir, packageId);
- if (Directory.Exists(targetDir))
- {
- Directory.Delete(targetDir, recursive: true);
- this.logger.LogInformation("ペンディング削除を処理: {PackageId}", packageId);
- }
- File.Delete(markerFile);
+ return;
}
- catch (Exception ex)
+
+ foreach (var markerFile in Directory.GetFiles(this.userPluginsDir, "*.pending-delete"))
{
- this.logger.LogWarning(ex, "ペンディング削除の処理に失敗: {MarkerFile}", markerFile);
+ try
+ {
+ var packageId = File.ReadAllText(markerFile);
+ var markerPackageId = Path.GetFileName(markerFile)[..^".pending-delete".Length];
+ if (!packageId.Equals(markerPackageId, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException("削除マーカーのパッケージIDがファイル名と一致しません。");
+ }
+
+ var targetDir = GetPackageDirectory(packageId);
+ if (Directory.Exists(targetDir))
+ {
+ Directory.Delete(targetDir, recursive: true);
+ this.logger.LogInformation("ペンディング削除を処理: {PackageId}", packageId);
+ }
+ File.Delete(markerFile);
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogWarning(ex, "ペンディング削除の処理に失敗: {MarkerFile}", markerFile);
+ }
}
}
+ finally
+ {
+ this.operationLock.Release();
+ }
}
///
@@ -315,9 +444,9 @@ public async Task> GetInstalledPackagesAsync
private async Task GetSearchUrlAsync(CancellationToken cancellationToken)
{
- var response = await this.httpClient.GetAsync(NuGetServiceIndexUrl, cancellationToken).ConfigureAwait(false);
+ using var response = await this.httpClient.GetAsync(NuGetServiceIndexUrl, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
- var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var index = await JsonSerializer.DeserializeAsync(content, JsonOptions, cancellationToken).ConfigureAwait(false)
?? throw new InvalidOperationException("NuGetサービスインデックスのデシリアライズに失敗しました。");
@@ -328,147 +457,165 @@ private async Task GetSearchUrlAsync(CancellationToken cancellationToken
return searchEntry.Id ?? throw new InvalidOperationException("NuGet検索サービスURLが空です。");
}
- private async Task DownloadFileAsync(string url, string destPath, IProgress? progress, CancellationToken cancellationToken)
+ private static InstalledManifest AddOrUpdatePackage(
+ InstalledManifest manifest,
+ string packageId,
+ string version)
{
- using var response = await this.httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
-
- var totalBytes = response.Content.Headers.ContentLength ?? -1;
- var downloadedBytes = 0L;
-
- using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- using var fileStream = File.Create(destPath);
- var buffer = new byte[81920];
- int bytesRead;
- while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0)
+ var packages = manifest.Packages.ToList();
+ var existing = packages.FindIndex(p => p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase));
+ var newEntry = new InstalledPackageInfo(packageId, version, DateTime.UtcNow);
+ if (existing >= 0)
{
- await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
- downloadedBytes += bytesRead;
- if (totalBytes > 0)
- {
- progress?.Report((double)downloadedBytes / totalBytes);
- }
+ packages[existing] = newEntry;
+ }
+ else
+ {
+ packages.Add(newEntry);
}
- }
- private static void ExtractPluginDlls(string nupkgPath, string targetDir)
- {
- using var archive = ZipFile.OpenRead(nupkgPath);
+ return new InstalledManifest([.. packages]);
+ }
- // 最適なTFMのlib/エントリを探す
- var libEntries = archive.Entries
- .Where(e => e.FullName.StartsWith("lib/", StringComparison.OrdinalIgnoreCase)
- && !string.IsNullOrEmpty(e.Name)
- && e.Name != "_._")
- .ToList();
+ private static InstalledManifest RemovePackage(InstalledManifest manifest, string packageId)
+ => new([.. manifest.Packages.Where(p =>
+ !p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase))]);
- if (!libEntries.Any())
+ private async Task LoadManifestAsync(CancellationToken cancellationToken)
+ {
+ if (!File.Exists(this.manifestPath))
{
- throw new InvalidOperationException("パッケージにlib/フォルダが見つかりませんでした。");
+ return new InstalledManifest([]);
}
- // TFMを選択(net10.0-windows > net10.0 > net9.0-windows > net9.0 > ... の優先順位)
- var tfmGroups = libEntries
- .GroupBy(e => e.FullName.Split('/')[1])
- .ToList();
-
- var selectedTfm = SelectBestTfm([.. tfmGroups.Select(g => g.Key)]);
- if (selectedTfm is null)
+ try
{
- throw new InvalidOperationException("互換性のあるターゲットフレームワークが見つかりませんでした。");
+ await using var fs = File.OpenRead(this.manifestPath);
+ return await JsonSerializer.DeserializeAsync(
+ fs,
+ JsonOptions,
+ cancellationToken).ConfigureAwait(false)
+ ?? throw new InvalidDataException("プラグインマニフェストが空です。");
}
-
- var selectedEntries = tfmGroups.First(g => g.Key == selectedTfm);
-
- foreach (var entry in selectedEntries)
+ catch (Exception ex) when (ex is not OperationCanceledException)
{
- var destPath = Path.Combine(targetDir, entry.Name);
- entry.ExtractToFile(destPath, overwrite: true);
+ this.logger.LogWarning(ex, "プラグインマニフェストの読み込みに失敗しました。");
+ throw new InvalidOperationException("プラグインマニフェストを読み込めませんでした。", ex);
}
}
- private static string? SelectBestTfm(string[] tfms)
+ private async Task SaveManifestAsync(InstalledManifest manifest, CancellationToken cancellationToken)
{
- // TFMの優先度リスト(.NET 10から降順、Windows版を優先)
- var orderedPrefixes = new[]
+ Directory.CreateDirectory(this.userPluginsDir);
+ var temporaryPath = $"{this.manifestPath}.tmp-{Guid.NewGuid():N}";
+ try
{
- "net10.0-windows",
- "net10.0",
- "net9.0-windows",
- "net9.0",
- "net8.0-windows",
- "net8.0",
- "net7.0-windows",
- "net7.0",
- "net6.0-windows",
- "net6.0",
- "netstandard2.1",
- "netstandard2.0",
- };
+ await using (var fs = new FileStream(
+ temporaryPath,
+ FileMode.CreateNew,
+ FileAccess.Write,
+ FileShare.None,
+ bufferSize: 4096,
+ useAsync: true))
+ {
+ await JsonSerializer.SerializeAsync(fs, manifest, JsonOptions, cancellationToken).ConfigureAwait(false);
+ await fs.FlushAsync(cancellationToken).ConfigureAwait(false);
+ }
- foreach (var prefix in orderedPrefixes)
+ ReplaceFile(temporaryPath, this.manifestPath);
+ }
+ finally
{
- // 完全一致または前方一致(例: net10.0-windows10.0.20348.0)
- var match = tfms.OrderByDescending(t => t).FirstOrDefault(t =>
- t.Equals(prefix, StringComparison.OrdinalIgnoreCase)
- || t.StartsWith(prefix + ".", StringComparison.OrdinalIgnoreCase)
- || t.StartsWith(prefix + "_", StringComparison.OrdinalIgnoreCase));
- if (match is not null)
- return match;
+ TryDeleteFile(temporaryPath);
}
+ }
- return tfms.FirstOrDefault();
+ private static async Task WriteTextAtomicallyAsync(
+ string destinationPath,
+ string content,
+ CancellationToken cancellationToken)
+ {
+ var temporaryPath = $"{destinationPath}.tmp-{Guid.NewGuid():N}";
+ try
+ {
+ await File.WriteAllTextAsync(temporaryPath, content, cancellationToken).ConfigureAwait(false);
+ ReplaceFile(temporaryPath, destinationPath);
+ }
+ finally
+ {
+ TryDeleteFile(temporaryPath);
+ }
}
- private async Task UpdateManifestAsync(string packageId, string version, CancellationToken cancellationToken)
+ private static void ReplaceFile(string sourcePath, string destinationPath)
+ => File.Move(sourcePath, destinationPath, overwrite: true);
+
+ private string GetPackageDirectory(string packageId)
{
- var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
- var packages = manifest.Packages.ToList();
- var existing = packages.FindIndex(p => p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase));
- var newEntry = new InstalledPackageInfo(packageId, version, DateTime.UtcNow);
- if (existing >= 0)
- packages[existing] = newEntry;
- else
- packages.Add(newEntry);
+ if (string.IsNullOrWhiteSpace(packageId)
+ || packageId is "." or ".."
+ || packageId.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0
+ || packageId.Contains(Path.DirectorySeparatorChar)
+ || packageId.Contains(Path.AltDirectorySeparatorChar))
+ {
+ throw new InvalidOperationException($"不正なNuGetパッケージIDです: {packageId}");
+ }
+
+ var root = this.userPluginsDir
+ .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ + Path.DirectorySeparatorChar;
+ var packageDirectory = Path.GetFullPath(Path.Combine(root, packageId));
+ if (!packageDirectory.StartsWith(root, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException($"不正なNuGetパッケージIDです: {packageId}");
+ }
- await SaveManifestAsync(new InstalledManifest([.. packages]), cancellationToken).ConfigureAwait(false);
+ return packageDirectory;
}
- private async Task RemoveFromManifestAsync(string packageId, CancellationToken cancellationToken)
+ private string GetPendingDeleteMarker(string packageId)
{
- var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
- var packages = manifest.Packages.Where(p => !p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)).ToList();
- await SaveManifestAsync(new InstalledManifest([.. packages]), cancellationToken).ConfigureAwait(false);
+ _ = GetPackageDirectory(packageId);
+ return Path.Combine(this.userPluginsDir, $"{packageId}.pending-delete");
}
- private async Task LoadManifestAsync(CancellationToken cancellationToken)
+ private static void TryDeleteDirectory(string directory)
{
try
{
- if (File.Exists(ManifestPath))
+ if (Directory.Exists(directory))
{
- using var fs = File.OpenRead(ManifestPath);
- var manifest = await JsonSerializer.DeserializeAsync(fs, JsonOptions, cancellationToken).ConfigureAwait(false);
- return manifest ?? new InstalledManifest([]);
+ Directory.Delete(directory, recursive: true);
}
}
- catch (Exception ex)
+ catch
{
- this.logger.LogWarning(ex, "プラグインマニフェストの読み込みに失敗しました。");
+ // 後始末の失敗は元の処理結果へ影響させない
}
- return new InstalledManifest([]);
}
- private static async Task SaveManifestAsync(InstalledManifest manifest, CancellationToken cancellationToken)
+ private static void TryDeleteFile(string path)
{
- Directory.CreateDirectory(UserPluginsDir);
- using var fs = File.Create(ManifestPath);
- await JsonSerializer.SerializeAsync(fs, manifest, JsonOptions, cancellationToken).ConfigureAwait(false);
+ try
+ {
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+ }
+ catch
+ {
+ // 後始末の失敗は元の処理結果へ影響させない
+ }
}
public void Dispose()
{
- this.httpClient.Dispose();
+ if (this.ownsHttpClient)
+ {
+ this.httpClient.Dispose();
+ }
+ this.operationLock.Dispose();
}
}
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index 527e005a..8059f580 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -2,6 +2,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
+using NuGet.Versioning;
using WindowTranslator.Properties;
using Wpf.Ui;
using Wpf.Ui.Extensions;
@@ -191,14 +192,13 @@ await this.dialogService.ShowAlertAsync(
private static bool IsNewerVersion(string latestVersion, string installedVersion)
{
- try
- {
- return Version.Parse(latestVersion) > Version.Parse(installedVersion);
- }
- catch
+ if (NuGetVersion.TryParse(latestVersion, out var latest)
+ && NuGetVersion.TryParse(installedVersion, out var installed))
{
- return string.Compare(latestVersion, installedVersion, StringComparison.OrdinalIgnoreCase) > 0;
+ return latest > installed;
}
+
+ return string.Compare(latestVersion, installedVersion, StringComparison.OrdinalIgnoreCase) > 0;
}
}
diff --git a/WindowTranslator/Properties/AssemblyInfo.cs b/WindowTranslator/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..cbbe402c
--- /dev/null
+++ b/WindowTranslator/Properties/AssemblyInfo.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("WindowTranslator.Tests")]
diff --git a/WindowTranslator/WindowTranslator.csproj b/WindowTranslator/WindowTranslator.csproj
index c34e6197..d1caa9a4 100644
--- a/WindowTranslator/WindowTranslator.csproj
+++ b/WindowTranslator/WindowTranslator.csproj
@@ -51,6 +51,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
diff --git a/docs/plugin.md b/docs/plugin.md
index cc9f31c6..bcbad419 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -30,7 +30,7 @@ cd WindowTranslator.Plugin.YourPlugin
YourName
説明文
- windowtranslator-plugin
+ $(PackageTags);windowtranslator-plugin
MIT
@@ -112,9 +112,21 @@ public class MyTranslateModule : ITranslateModule { ... }
- Windows: `%USERPROFILE%\.wt\plugins\{PackageId}\`
+## アプリからインストールする
+
+1. WindowTranslator の設定を開きます。
+2. 「プラグインストア」タブを選択します。
+3. 利用するプラグインの「インストール」を選択します。
+4. インストール完了後に WindowTranslator を再起動します。
+
+NuGetパッケージで宣言されたランタイム依存関係も再帰的に取得されます。
+同じ依存パッケージに両立しないバージョン条件がある場合は、既存の
+プラグイン配置を変更せずにインストールを中止します。
+
## 注意事項
- プラグインは .NET 10 以上をターゲットにしてください
- `true` を必ず設定してください
- ホスト側で既に提供されているパッケージは `ExcludeAssets="runtime"` を設定し、DLL を重複させないようにしてください
-- プラグインに必要な独自の依存 DLL はすべて `lib/net10.0/` フォルダに含めてください
+- 通常のランタイム依存は `PackageReference` として宣言してください
+- パッケージ固有の追加ファイルは、実行時に必要な相対ディレクトリを保って `lib/net10.0/` に含めてください
From 5fa132bfe396c09a959c5690f3a2eb8fb23e46a2 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sat, 1 Aug 2026 02:56:25 +0900
Subject: [PATCH 07/43] Address NuGet plugin lifecycle and packaging feedback
---
.github/workflows/dotnet-desktop.yml | 22 +-
.github/workflows/dotnet-package.yml | 42 ++-
ColorThief/ColorThief/ColorThief.csproj | 4 +
Plugins/Directory.Build.props | 6 +-
Plugins/Directory.Build.targets | 12 +-
...tor.Plugin.BergamotTranslatorPlugin.csproj | 2 +
...wTranslator.Plugin.ColorThiefPlugin.csproj | 2 +
...nslator.Plugin.DeepLTranslatePlugin.csproj | 2 +
...WindowTranslator.Plugin.DummyPlugin.csproj | 5 +
.../WindowTranslator.Plugin.FoMPlugin.csproj | 2 +
...anslator.Plugin.GitHubCopilotPlugin.csproj | 2 +
...dowTranslator.Plugin.GoogleAIPlugin.csproj | 2 +
...lator.Plugin.GoogleAppsSctiptPlugin.csproj | 2 +
.../WindowTranslator.Plugin.LLMPlugin.csproj | 2 +
...indowTranslator.Plugin.OneOcrPlugin.csproj | 2 +
...WindowTranslator.Plugin.PLaMoPlugin.csproj | 6 +-
...ranslator.Plugin.TesseractOCRPlugin.csproj | 2 +
.../NuGetPluginServiceTests.cs | 355 ++++++++++++++++--
.../UserSettingsConfigurationTests.cs | 69 ++++
.../PluginStore/NuGetPackageInstaller.cs | 68 +++-
.../Modules/PluginStore/NuGetPluginCatalog.cs | 152 ++++++--
.../Modules/PluginStore/NuGetPluginService.cs | 351 +++++------------
.../PluginStore/PluginStoreViewModel.cs | 24 +-
WindowTranslator/Program.cs | 89 ++++-
.../Properties/Resources.Designer.cs | 258 ++++++-------
WindowTranslator/Properties/Resources.ar.resx | 2 +-
WindowTranslator/Properties/Resources.cs.resx | 2 +-
WindowTranslator/Properties/Resources.de.resx | 2 +-
WindowTranslator/Properties/Resources.en.resx | 2 +-
WindowTranslator/Properties/Resources.es.resx | 2 +-
WindowTranslator/Properties/Resources.fa.resx | 2 +-
.../Properties/Resources.fil.resx | 2 +-
WindowTranslator/Properties/Resources.fr.resx | 2 +-
WindowTranslator/Properties/Resources.hi.resx | 4 +-
WindowTranslator/Properties/Resources.hu.resx | 2 +-
WindowTranslator/Properties/Resources.id.resx | 2 +-
WindowTranslator/Properties/Resources.ko.resx | 2 +-
WindowTranslator/Properties/Resources.ms.resx | 2 +-
WindowTranslator/Properties/Resources.pl.resx | 2 +-
.../Properties/Resources.pt-BR.resx | 2 +-
WindowTranslator/Properties/Resources.resx | 2 +-
WindowTranslator/Properties/Resources.ru.resx | 2 +-
WindowTranslator/Properties/Resources.th.resx | 2 +-
WindowTranslator/Properties/Resources.tr.resx | 2 +-
WindowTranslator/Properties/Resources.vi.resx | 2 +-
.../Properties/Resources.zh-CN.resx | 2 +-
.../Properties/Resources.zh-TW.resx | 2 +-
docs/plugin.md | 19 +-
48 files changed, 1016 insertions(+), 532 deletions(-)
create mode 100644 WindowTranslator.Tests/UserSettingsConfigurationTests.cs
diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml
index 234fe11c..95f68e4c 100644
--- a/.github/workflows/dotnet-desktop.yml
+++ b/.github/workflows/dotnet-desktop.yml
@@ -125,9 +125,6 @@ jobs:
versionSpec: "6.x"
- id: gitversion
uses: gittools/actions/gitversion/execute@v4.7.0
- - uses: Jimver/cuda-toolkit@v0.2.30
- with:
- cuda: '12.9.0'
- run: |
dotnet publish WindowTranslator -c Release -o publish --sc ${{ matrix.self }} `
-p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
@@ -137,14 +134,27 @@ jobs:
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- run: |
- $folders = Get-ChildItem -Path "Plugins" -Directory | Where-Object { $_.Name -notmatch "Dummy" -and $_.Name -notmatch "Tests?" }
- for ($i = 0; $i -lt $folders.Count; $i++) {
- dotnet publish $folders[$i].FullName -c Release -o "publish\plugins\$($folders[$i].Name)" `
+ $projects = Get-ChildItem -Path "Plugins\WindowTranslator.Plugin.*\*.csproj" |
+ Where-Object { $_.Directory.Name -notmatch "Dummy" -and $_.Directory.Name -notmatch "Tests?" }
+ foreach ($project in $projects) {
+ $propertyOutput = dotnet msbuild $project.FullName -nologo -getProperty:ExcludeFromAppBundle
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+ $excludeFromAppBundle = ($propertyOutput | Select-Object -Last 1).Trim()
+ if ($excludeFromAppBundle -eq "true") {
+ Write-Host "Skip app bundle: $($project.Name)"
+ continue
+ }
+ dotnet publish $project.FullName -c Release -o "publish\plugins\$($project.Directory.Name)" `
-p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} `
-p:DecryptKey="${{ secrets.WINDOWTRANSLATOR_DECRYPTKEY }}"
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
}
- uses: actions/download-artifact@v8
if: ${{ needs.docs.result == 'success' }}
diff --git a/.github/workflows/dotnet-package.yml b/.github/workflows/dotnet-package.yml
index 32314746..685cafa9 100644
--- a/.github/workflows/dotnet-package.yml
+++ b/.github/workflows/dotnet-package.yml
@@ -34,16 +34,36 @@ jobs:
-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
- Get-ChildItem Plugins\WindowTranslator.Plugin.*\*.csproj |
- Where-Object { $_.Directory.Name -notlike '*.Tests' } |
- ForEach-Object {
- dotnet pack $_.FullName -c Release -o pack `
- -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
- -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
- -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
- -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+ dotnet pack ColorThief\ColorThief -c Release -o pack `
+ -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
+ -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
+ -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
+ -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+ $projects = Get-ChildItem Plugins\WindowTranslator.Plugin.*\*.csproj |
+ Where-Object { $_.Directory.Name -notlike '*.Tests' }
+ foreach ($project in $projects) {
+ $propertyOutput = dotnet msbuild $project.FullName -nologo -getProperty:IsPackable
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
+ }
+ $isPackable = ($propertyOutput | Select-Object -Last 1).Trim()
+ if ($isPackable -ne "true") {
+ Write-Host "Skip NuGet package: $($project.Name)"
+ continue
+ }
+ dotnet pack $project.FullName -c Release -o pack `
+ -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
+ -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
+ -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
+ -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
}
+ }
dotnet nuget push pack\*.nupkg -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate
diff --git a/ColorThief/ColorThief/ColorThief.csproj b/ColorThief/ColorThief/ColorThief.csproj
index 556393bf..bf9a9540 100644
--- a/ColorThief/ColorThief/ColorThief.csproj
+++ b/ColorThief/ColorThief/ColorThief.csproj
@@ -2,6 +2,10 @@
net10.0-windows10.0.20348.0
+ WindowTranslator.ColorThief
+ WindowTranslator ColorThief Support Library
+ Color extraction support library for the WindowTranslator ColorThief plugin.
+ $(PackageTags);ColorThief
enable
enable
StudioFreesia.ColorThief
diff --git a/Plugins/Directory.Build.props b/Plugins/Directory.Build.props
index 3295561e..e50f2937 100644
--- a/Plugins/Directory.Build.props
+++ b/Plugins/Directory.Build.props
@@ -8,10 +8,8 @@
- https://github.com/Freeesia/WindowTranslator
- Freeesia
-
- false
+ false
false
diff --git a/Plugins/Directory.Build.targets b/Plugins/Directory.Build.targets
index ed78c408..f880e7be 100644
--- a/Plugins/Directory.Build.targets
+++ b/Plugins/Directory.Build.targets
@@ -2,16 +2,20 @@
-
+
$(PackageTags);windowtranslator-plugin
- $(TargetsForTfmSpecificBuildOutput);AddPluginRuntimeAssetsToPackage
+ $(TargetsForTfmSpecificBuildOutput);AddPluginRuntimeAssetsToPackage
-
+
<_PluginWinX64RuntimeAsset Include="$(TargetDir)runtimes\win-x64\**\*" />
<_PluginWinRuntimeAsset Include="$(TargetDir)runtimes\win\**\*" />
diff --git a/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj b/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj
index b13dc3e6..a82c827c 100644
--- a/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj
@@ -2,6 +2,8 @@
net10.0
+ WindowTranslator Bergamot Translator Plugin
+ Offline neural machine translation for WindowTranslator using Bergamot.
enable
enable
true
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj b/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj
index 4fe79cd6..3d9529ea 100644
--- a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj
@@ -1,6 +1,8 @@
net10.0-windows10.0.20348.0
+ WindowTranslator ColorThief Plugin
+ Detects readable foreground and background colors for translated text.
diff --git a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
index 8c02a33f..96f41f02 100644
--- a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
@@ -1,6 +1,8 @@
+ WindowTranslator DeepL Translator Plugin
+ Translation for WindowTranslator using the DeepL API.
true
diff --git a/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj b/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj
index 61718a1f..7fb550e8 100644
--- a/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj
@@ -1,3 +1,8 @@
+
+
+ false
+
+
diff --git a/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj b/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
index e0c9fe00..f4b22776 100644
--- a/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
@@ -1,6 +1,8 @@
net10.0-windows10.0.20348.0
+ WindowTranslator Fields of Mistria Filter Plugin
+ Context-aware translation filtering for Fields of Mistria.
true
true
diff --git a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
index e9ea1352..428fe634 100644
--- a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
@@ -1,6 +1,8 @@
net10.0-windows10.0.20348.0
+ WindowTranslator GitHub Copilot Translator Plugin
+ Translation for WindowTranslator using GitHub Copilot.
true
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
index e15c28c5..beac88eb 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
@@ -2,6 +2,8 @@
net10.0-windows10.0.20348.0
+ WindowTranslator Google AI Plugin
+ Translation, OCR, and text correction for WindowTranslator using Google AI.
true
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
index 6177f6ba..5eb0def3 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
@@ -1,5 +1,7 @@
+ WindowTranslator Google Apps Script Translator Plugin
+ Translation for WindowTranslator through Google Apps Script.
true
diff --git a/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj b/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
index a37c1d24..77b4ff35 100644
--- a/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
@@ -1,6 +1,8 @@
net10.0-windows10.0.20348.0
+ WindowTranslator LLM Plugin
+ Translation, OCR, and text correction through OpenAI-compatible language models.
true
diff --git a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj
index e6bb20f4..87c87e52 100644
--- a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj
@@ -1,6 +1,8 @@
net10.0-windows10.0.20348.0
+ WindowTranslator OneOCR Plugin
+ OCR for WindowTranslator using the Windows OneOCR engine.
true
diff --git a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
index bc77e53a..41a9cb76 100644
--- a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
@@ -2,11 +2,15 @@
net10.0
+ WindowTranslator PLaMo Translator Plugin
+ Local PLaMo translation for WindowTranslator using LLamaSharp and CUDA.
true
+ false
+ true
-
+
diff --git a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
index b0e61f0e..75de048b 100644
--- a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
@@ -2,6 +2,8 @@
net10.0-windows10.0.20348.0
+ WindowTranslator Tesseract OCR Plugin
+ OCR for WindowTranslator using the Tesseract engine.
true
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index c87f2313..d1eade8e 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -5,6 +5,9 @@
using System.Text.Json;
using System.Xml.Linq;
using Microsoft.Extensions.Logging.Abstractions;
+using NuGet.Versioning;
+using Weikio.PluginFramework.Catalogs;
+using WindowTranslator.Modules;
using WindowTranslator.Modules.PluginStore;
namespace WindowTranslator.Tests;
@@ -167,7 +170,7 @@ await File.ReadAllTextAsync(
}
[Fact]
- public async Task ReinstallAfterUninstallRemovesThePendingDeletionMarker()
+ public async Task UninstallRemovesManagedFilesImmediatelyAndAllowsManualReinstall()
{
var testDirectory = CreateTestDirectory();
try
@@ -201,13 +204,10 @@ public async Task ReinstallAfterUninstallRemovesThePendingDeletionMarker()
await service.InstallPackageAsync("Root.Plugin", "1.0.0");
await service.UninstallPackageAsync("Root.Plugin");
- var markerPath = Path.Combine(testDirectory, "Root.Plugin.pending-delete");
- Assert.True(File.Exists(markerPath));
+ Assert.False(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+ Assert.Empty(await service.GetInstalledPackagesAsync());
await service.InstallPackageAsync("Root.Plugin", "2.0.0");
- Assert.False(File.Exists(markerPath));
-
- service.ProcessPendingDeletions();
Assert.True(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
var installed = Assert.Single(await service.GetInstalledPackagesAsync());
Assert.Equal("2.0.0", installed.Version);
@@ -218,6 +218,56 @@ public async Task ReinstallAfterUninstallRemovesThePendingDeletionMarker()
}
}
+ [Fact]
+ public async Task UninstallRestoresManagedFilesWhenManifestUpdateFails()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Root.Plugin",
+ "1.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "1.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "version-one"u8.ToArray(),
+ }));
+
+ using var client = new HttpClient(handler);
+ using var service = CreateService(client, testDirectory);
+ await service.InstallPackageAsync("Root.Plugin", "1.0.0");
+
+ var manifestPath = Path.Combine(testDirectory, "nuget-manifest.json");
+ using (var manifestLock = new FileStream(
+ manifestPath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read))
+ {
+ var exception = await Record.ExceptionAsync(
+ () => service.UninstallPackageAsync("Root.Plugin"));
+ Assert.True(
+ exception is IOException or UnauthorizedAccessException,
+ $"Unexpected exception: {exception}");
+ }
+
+ Assert.True(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+ Assert.Empty(Directory.GetDirectories(
+ testDirectory,
+ "Root.Plugin.uninstalling-*"));
+ var installed = Assert.Single(await service.GetInstalledPackagesAsync());
+ Assert.Equal("1.0.0", installed.Version);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
[Fact]
public async Task DependencyWithIncompatibleLibStillInstallsCompatibleNativeAssets()
{
@@ -318,7 +368,136 @@ await File.ReadAllTextAsync(
}
[Fact]
- public void CatalogCopyIncludesLegacyRootFilesAndSkipsManagementState()
+ public async Task ExistingManifestLoadsInstalledPackages()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(testDirectory, "nuget-manifest.json"),
+ JsonSerializer.Serialize(new
+ {
+ Packages = new[]
+ {
+ new
+ {
+ Id = "Legacy.Plugin",
+ Version = "1.0.0",
+ },
+ },
+ }));
+
+ using var handler = new InMemoryNuGetHandler();
+ using var client = new HttpClient(handler);
+ using var service = CreateService(client, testDirectory);
+
+ var installed = Assert.Single(await service.GetInstalledPackagesAsync());
+
+ Assert.Equal("Legacy.Plugin", installed.Id);
+ Assert.Equal("1.0.0", installed.Version);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task InstallRejectsPackageRequiringNewerHostAbstractions()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Root.Plugin",
+ "2.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "2.0.0",
+ [
+ new(
+ "WindowTranslator.Abstractions",
+ "[2.0.0, 3.0.0)",
+ Exclude: "Runtime"),
+ ],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "root"u8.ToArray(),
+ }));
+
+ using var client = new HttpClient(handler);
+ using var service = CreateService(
+ client,
+ testDirectory,
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["WindowTranslator.Abstractions"] = NuGetVersion.Parse("1.5.0"),
+ });
+
+ var exception = await Assert.ThrowsAsync(
+ () => service.InstallPackageAsync("Root.Plugin", "2.0.0"));
+
+ Assert.Contains("WindowTranslator.Abstractions", exception.Message);
+ Assert.Contains("[2.0.0, 3.0.0)", exception.Message);
+ Assert.False(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+ Assert.Empty(await service.GetInstalledPackagesAsync());
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task InstallAcceptsCompatibleHostAbstractionsWithoutDownloadingIt()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Root.Plugin",
+ "1.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "1.0.0",
+ [
+ new(
+ "WindowTranslator.Abstractions",
+ "[1.0.0, 2.0.0)"),
+ ],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "root"u8.ToArray(),
+ }));
+
+ using var client = new HttpClient(handler);
+ using var service = CreateService(
+ client,
+ testDirectory,
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["WindowTranslator.Abstractions"] = NuGetVersion.Parse("1.5.0"),
+ });
+
+ await service.InstallPackageAsync("Root.Plugin", "1.0.0");
+
+ Assert.True(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+ Assert.DoesNotContain(
+ handler.RequestedPaths,
+ path => path.Contains(
+ "windowtranslator.abstractions",
+ StringComparison.OrdinalIgnoreCase));
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public void CatalogSynchronizationCopiesOnlyChangesAndRemovesStaleFiles()
{
var sourceDirectory = CreateTestDirectory();
var destinationDirectory = CreateTestDirectory();
@@ -327,41 +506,114 @@ public void CatalogCopyIncludesLegacyRootFilesAndSkipsManagementState()
File.WriteAllText(Path.Combine(sourceDirectory, "Legacy.Plugin.dll"), "legacy");
File.WriteAllText(Path.Combine(sourceDirectory, "nuget-manifest.json"), "{}");
File.WriteAllText(Path.Combine(sourceDirectory, "nuget-manifest.json.tmp-test"), "{}");
- File.WriteAllText(Path.Combine(sourceDirectory, "Root.Plugin.pending-delete"), "Root.Plugin");
- File.WriteAllText(
- Path.Combine(sourceDirectory, "Root.Plugin.pending-delete.tmp-test"),
- "Root.Plugin");
Directory.CreateDirectory(Path.Combine(sourceDirectory, "Root.Plugin"));
- File.WriteAllText(
- Path.Combine(sourceDirectory, "Root.Plugin", "Root.Plugin.dll"),
- "plugin");
+ var sourcePluginPath =
+ Path.Combine(sourceDirectory, "Root.Plugin", "Root.Plugin.dll");
+ File.WriteAllText(sourcePluginPath, "plugin-new");
+ var unchangedSourcePath =
+ Path.Combine(sourceDirectory, "Root.Plugin", "Unchanged.dll");
+ File.WriteAllText(unchangedSourcePath, "unchanged");
+ Directory.CreateDirectory(Path.Combine(sourceDirectory, "Empty.Plugin"));
Directory.CreateDirectory(Path.Combine(sourceDirectory, "Root.Plugin.backup-test"));
File.WriteAllText(
Path.Combine(sourceDirectory, "Root.Plugin.backup-test", "old.dll"),
"old");
Directory.CreateDirectory(Path.Combine(sourceDirectory, ".Root.Plugin.installing-test"));
+ Directory.CreateDirectory(Path.Combine(sourceDirectory, "Root.Plugin.uninstalling-test"));
Directory.CreateDirectory(Path.Combine(destinationDirectory, "Root.Plugin"));
+ var destinationPluginPath =
+ Path.Combine(destinationDirectory, "Root.Plugin", "Root.Plugin.dll");
+ File.WriteAllText(destinationPluginPath, "plugin-old");
+ var unchangedDestinationPath =
+ Path.Combine(destinationDirectory, "Root.Plugin", "Unchanged.dll");
+ File.WriteAllText(unchangedDestinationPath, "unchanged");
+ var unchangedTimestamp = DateTime.UtcNow.AddMinutes(-5);
+ File.SetLastWriteTimeUtc(unchangedSourcePath, unchangedTimestamp);
+ File.SetLastWriteTimeUtc(unchangedDestinationPath, unchangedTimestamp);
+ File.SetCreationTimeUtc(unchangedSourcePath, unchangedTimestamp);
+ File.SetCreationTimeUtc(unchangedDestinationPath, unchangedTimestamp);
+ File.SetLastWriteTimeUtc(sourcePluginPath, unchangedTimestamp);
+ File.SetLastWriteTimeUtc(destinationPluginPath, unchangedTimestamp);
+ File.SetCreationTimeUtc(
+ sourcePluginPath,
+ unchangedTimestamp.AddMinutes(2));
+ File.SetCreationTimeUtc(
+ destinationPluginPath,
+ unchangedTimestamp.AddMinutes(-2));
File.WriteAllText(
- Path.Combine(destinationDirectory, "Root.Plugin", "Root.Plugin.dll"),
- "existing");
+ Path.Combine(destinationDirectory, "Root.Plugin", "Removed.dll"),
+ "stale");
+ Directory.CreateDirectory(Path.Combine(destinationDirectory, "Removed.Plugin"));
+ File.WriteAllText(
+ Path.Combine(destinationDirectory, "Removed.Plugin", "Removed.Plugin.dll"),
+ "stale");
+ File.WriteAllText(
+ Path.Combine(destinationDirectory, "nuget-manifest.json"),
+ "{}");
- NuGetPluginCatalog.CopyPluginFiles(sourceDirectory, destinationDirectory);
+ using var unchangedFileLock = new FileStream(
+ unchangedDestinationPath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read);
+ NuGetPluginCatalog.SynchronizePluginFiles(
+ sourceDirectory,
+ destinationDirectory);
+ using var synchronizedFileLock = new FileStream(
+ destinationPluginPath,
+ FileMode.Open,
+ FileAccess.Read,
+ FileShare.Read);
+ NuGetPluginCatalog.SynchronizePluginFiles(
+ sourceDirectory,
+ destinationDirectory);
Assert.True(File.Exists(Path.Combine(destinationDirectory, "Legacy.Plugin.dll")));
Assert.Equal(
- "existing",
- File.ReadAllText(
- Path.Combine(destinationDirectory, "Root.Plugin", "Root.Plugin.dll")));
+ "plugin-new",
+ File.ReadAllText(destinationPluginPath));
+ Assert.Equal("unchanged", File.ReadAllText(unchangedDestinationPath));
+ Assert.False(File.Exists(
+ Path.Combine(destinationDirectory, "Root.Plugin", "Removed.dll")));
+ Assert.False(Directory.Exists(
+ Path.Combine(destinationDirectory, "Removed.Plugin")));
+ Assert.True(Directory.Exists(
+ Path.Combine(destinationDirectory, "Empty.Plugin")));
Assert.False(File.Exists(Path.Combine(destinationDirectory, "nuget-manifest.json")));
Assert.False(File.Exists(
Path.Combine(destinationDirectory, "nuget-manifest.json.tmp-test")));
- Assert.False(File.Exists(Path.Combine(destinationDirectory, "Root.Plugin.pending-delete")));
- Assert.False(File.Exists(
- Path.Combine(destinationDirectory, "Root.Plugin.pending-delete.tmp-test")));
Assert.False(Directory.Exists(
Path.Combine(destinationDirectory, "Root.Plugin.backup-test")));
Assert.False(Directory.Exists(
Path.Combine(destinationDirectory, ".Root.Plugin.installing-test")));
+ Assert.False(Directory.Exists(
+ Path.Combine(destinationDirectory, "Root.Plugin.uninstalling-test")));
+ }
+ finally
+ {
+ DeleteTestDirectory(sourceDirectory);
+ DeleteTestDirectory(destinationDirectory);
+ }
+ }
+
+ [Fact]
+ public void CatalogSynchronizationClearsStaleFilesWhenSourceIsMissing()
+ {
+ var sourceDirectory = CreateTestDirectory();
+ var destinationDirectory = CreateTestDirectory();
+ try
+ {
+ Directory.Delete(sourceDirectory);
+ Directory.CreateDirectory(Path.Combine(destinationDirectory, "Removed.Plugin"));
+ File.WriteAllText(
+ Path.Combine(destinationDirectory, "Removed.Plugin", "Removed.Plugin.dll"),
+ "stale");
+
+ NuGetPluginCatalog.SynchronizePluginFiles(
+ sourceDirectory,
+ destinationDirectory);
+
+ Assert.Empty(Directory.EnumerateFileSystemEntries(destinationDirectory));
}
finally
{
@@ -370,6 +622,48 @@ public void CatalogCopyIncludesLegacyRootFilesAndSkipsManagementState()
}
}
+ [Fact]
+ public async Task CatalogLoadsARealAssemblyFromAPackageSubdirectory()
+ {
+ var sourceDirectory = CreateTestDirectory();
+ var tempDirectory = CreateTestDirectory();
+ try
+ {
+ var packageDirectory = Path.Combine(sourceDirectory, "Catalog.Probe");
+ Directory.CreateDirectory(packageDirectory);
+ var testAssemblyPath = typeof(NuGetPluginServiceTests).Assembly.Location;
+ File.Copy(
+ testAssemblyPath,
+ Path.Combine(packageDirectory, Path.GetFileName(testAssemblyPath)));
+
+ var options = new FolderPluginCatalogOptions();
+ options.TypeFinderOptions.TypeFinderCriterias.Clear();
+ options.TypeFinderOptions.TypeFinderCriterias.Add(new()
+ {
+ Query = static (_, type) =>
+ type.Name == nameof(CatalogProbeTranslateModule),
+ });
+ options.PluginLoadContextOptions.AdditionalRuntimePaths =
+ [AppContext.BaseDirectory];
+ var catalog = new NuGetPluginCatalog(
+ sourceDirectory,
+ tempDirectory,
+ options);
+
+ await catalog.Initialize();
+
+ Assert.True(catalog.IsInitialized);
+ Assert.Contains(
+ catalog.GetPlugins(),
+ plugin => plugin.Type.Name == nameof(CatalogProbeTranslateModule));
+ }
+ finally
+ {
+ DeleteTestDirectory(sourceDirectory);
+ DeleteTestDirectory(tempDirectory);
+ }
+ }
+
[Fact]
public void FrameworkSelectionPrefersTheCompatibleWindowsTarget()
{
@@ -380,11 +674,15 @@ public void FrameworkSelectionPrefersTheCompatibleWindowsTarget()
Assert.Null(NuGetPackageInstaller.SelectBestTfm(["net48"]));
}
- private static NuGetPluginService CreateService(HttpClient client, string pluginDirectory)
+ private static NuGetPluginService CreateService(
+ HttpClient client,
+ string pluginDirectory,
+ IReadOnlyDictionary? hostPackageVersions = null)
=> new(
NullLogger.Instance,
client,
- pluginDirectory);
+ pluginDirectory,
+ hostPackageVersions: hostPackageVersions);
private static byte[] CreatePackage(
string id,
@@ -519,3 +817,10 @@ protected override Task SendAsync(
}
}
}
+
+public sealed class CatalogProbeTranslateModule : ITranslateModule
+{
+ public ValueTask TranslateAsync(TextInfo[] srcTexts)
+ => ValueTask.FromResult(
+ Enumerable.Repeat(string.Empty, srcTexts.Length).ToArray());
+}
diff --git a/WindowTranslator.Tests/UserSettingsConfigurationTests.cs b/WindowTranslator.Tests/UserSettingsConfigurationTests.cs
new file mode 100644
index 00000000..c3ae70c3
--- /dev/null
+++ b/WindowTranslator.Tests/UserSettingsConfigurationTests.cs
@@ -0,0 +1,69 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging.Abstractions;
+using WindowTranslator.Modules;
+using WindowTranslator.Stores;
+
+namespace WindowTranslator.Tests;
+
+public sealed class UserSettingsConfigurationTests
+{
+ [Fact]
+ public void UserSettingsIgnoresPluginParametersThatHaveNoLoadedType()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Common:ViewMode"] = nameof(ViewMode.Capture),
+ ["Targets::Font"] = "Default Font",
+ ["Targets:game:Font"] = "Test Font",
+ ["Targets:game:SelectedPlugins:ITranslateModule"] = "MissingTranslator",
+ ["Targets:game:PluginParams:MissingOptions:ApiKey"] = "secret",
+ })
+ .Build();
+ var settings = new UserSettings();
+
+ new global::ConfigureUserSettings(configuration).Configure(settings);
+
+ Assert.Equal(ViewMode.Capture, settings.Common.ViewMode);
+ Assert.Equal("Default Font", settings.Targets[string.Empty].Font);
+ var target = settings.Targets["game"];
+ Assert.Equal("Test Font", target.Font);
+ Assert.Equal(
+ "MissingTranslator",
+ target.SelectedPlugins[nameof(ITranslateModule)]);
+ Assert.Empty(target.PluginParams);
+ }
+
+ [Fact]
+ public void InvalidLoadedPluginParameterIsIgnoredWithoutChangingDefaults()
+ {
+ var configuration = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["Targets:game:PluginParams:InvalidPluginParam:RetryCount"] =
+ "not-an-integer",
+ })
+ .Build();
+ var options = new InvalidPluginParam();
+ var configure = new global::ConfigurePluginParam(
+ configuration,
+ new TestProcessInfoStore("game"),
+ NullLogger>.Instance);
+
+ configure.Configure(options);
+
+ Assert.Equal(7, options.RetryCount);
+ }
+
+ public sealed class InvalidPluginParam : IPluginParam
+ {
+ public int RetryCount { get; set; } = 7;
+ }
+
+ private sealed class TestProcessInfoStore(string name) : IProcessInfoStore
+ {
+ public IntPtr MainWindowHandle => IntPtr.Zero;
+
+ public string Name { get; } = name;
+ }
+}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
index 51822ef2..208ff456 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
@@ -12,7 +12,10 @@ namespace WindowTranslator.Modules.PluginStore;
///
/// NuGetパッケージとそのランタイム依存関係を、プラグインフォルダへ展開します。
///
-internal sealed class NuGetPackageInstaller(HttpClient httpClient, ILogger logger)
+internal sealed class NuGetPackageInstaller(
+ HttpClient httpClient,
+ ILogger logger,
+ IReadOnlyDictionary? hostPackageVersions = null)
{
private const string FlatContainerBase = "https://api.nuget.org/v3-flatcontainer";
@@ -39,6 +42,8 @@ internal sealed class NuGetPackageInstaller(HttpClient httpClient, ILogger logge
private readonly HttpClient httpClient = httpClient;
private readonly ILogger logger = logger;
+ private readonly IReadOnlyDictionary hostPackageVersions =
+ hostPackageVersions ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
public async Task InstallAsync(
string packageId,
@@ -173,10 +178,20 @@ await DownloadPackageAsync(
packagePath,
currentId.Equals(rootPackageId, StringComparison.OrdinalIgnoreCase) ? progress : null,
cancellationToken).ConfigureAwait(false);
+ ValidateHostPackageDependencies(
+ packagePath,
+ currentId,
+ resolvedVersion,
+ this.hostPackageVersions);
artifacts[currentId] = new PackageArtifact(currentId, resolvedVersion, packagePath);
foreach (var dependency in ReadRuntimeDependencies(packagePath))
{
+ if (this.hostPackageVersions.ContainsKey(dependency.Id))
+ {
+ continue;
+ }
+
AddConstraint(dependency.Id, currentId, dependency.VersionRange);
}
}
@@ -284,6 +299,14 @@ private async Task DownloadPackageAsync(
}
private static List ReadRuntimeDependencies(string packagePath)
+ => ReadDependencies(packagePath, runtimeOnly: true);
+
+ private static List ReadPackageDependencies(string packagePath)
+ => ReadDependencies(packagePath, runtimeOnly: false);
+
+ private static List ReadDependencies(
+ string packagePath,
+ bool runtimeOnly)
{
using var archive = ZipFile.OpenRead(packagePath);
var nuspecEntry = archive.Entries.FirstOrDefault(e =>
@@ -302,7 +325,8 @@ private static List ReadRuntimeDependencies(string packagePat
var result = new List();
result.AddRange(ParseDependencyElements(
- dependencies.Elements().Where(e => e.Name.LocalName == "dependency")));
+ dependencies.Elements().Where(e => e.Name.LocalName == "dependency"),
+ runtimeOnly));
var groups = dependencies.Elements()
.Where(e => e.Name.LocalName == "group")
@@ -327,7 +351,8 @@ private static List ReadRuntimeDependencies(string packagePat
frameworkGroups.First(g => string.Equals(
g.Framework,
selectedFramework,
- StringComparison.OrdinalIgnoreCase)).Element.Elements()));
+ StringComparison.OrdinalIgnoreCase)).Element.Elements(),
+ runtimeOnly));
return result;
}
}
@@ -337,19 +362,24 @@ private static List ReadRuntimeDependencies(string packagePat
|| g.Framework.Equals("any", StringComparison.OrdinalIgnoreCase));
if (fallbackGroup.Element is not null)
{
- result.AddRange(ParseDependencyElements(fallbackGroup.Element.Elements()));
+ result.AddRange(ParseDependencyElements(
+ fallbackGroup.Element.Elements(),
+ runtimeOnly));
return result;
}
throw new InvalidOperationException("互換性のある依存関係グループが見つかりませんでした。");
}
- private static IEnumerable ParseDependencyElements(IEnumerable elements)
+ private static IEnumerable ParseDependencyElements(
+ IEnumerable elements,
+ bool runtimeOnly)
{
foreach (var element in elements.Where(e => e.Name.LocalName == "dependency"))
{
var id = element.Attribute("id")?.Value;
- if (string.IsNullOrWhiteSpace(id) || !IncludesRuntimeAssets(element))
+ if (string.IsNullOrWhiteSpace(id)
+ || runtimeOnly && !IncludesRuntimeAssets(element))
{
continue;
}
@@ -361,6 +391,32 @@ private static IEnumerable ParseDependencyElements(IEnumerabl
}
}
+ private static void ValidateHostPackageDependencies(
+ string packagePath,
+ string packageId,
+ NuGetVersion packageVersion,
+ IReadOnlyDictionary hostPackageVersions)
+ {
+ if (hostPackageVersions.Count == 0)
+ {
+ return;
+ }
+
+ foreach (var dependency in ReadPackageDependencies(packagePath))
+ {
+ if (!hostPackageVersions.TryGetValue(dependency.Id, out var hostVersion)
+ || dependency.VersionRange.Satisfies(hostVersion))
+ {
+ continue;
+ }
+
+ throw new InvalidOperationException(
+ $"プラグイン {packageId} {packageVersion} は "
+ + $"{dependency.Id} {dependency.VersionRange} を必要としますが、"
+ + $"実行中のWindowTranslatorが提供するバージョンは {hostVersion} です。");
+ }
+ }
+
private static bool IncludesRuntimeAssets(XElement dependency)
{
var excluded = SplitAssets(dependency.Attribute("exclude")?.Value);
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 9126a1c6..b1d82876 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -35,17 +35,7 @@ internal NuGetPluginCatalog(string sourceDir, string tempDir, FolderPluginCatalo
///
public async Task Initialize()
{
- // ロック解除のために一時フォルダを削除してからコピー
- if (Directory.Exists(this.tempDir))
- {
- Directory.Delete(this.tempDir, recursive: true);
- }
- Directory.CreateDirectory(this.tempDir);
-
- if (Directory.Exists(this.sourceDir))
- {
- CopyPluginFiles(this.sourceDir, this.tempDir);
- }
+ SynchronizePluginFiles(this.sourceDir, this.tempDir);
await this.innerCatalog.Initialize().ConfigureAwait(false);
}
@@ -56,62 +46,154 @@ public async Task Initialize()
///
public Plugin Get(string name, Version version) => this.innerCatalog.Get(name, version);
- internal static void CopyPluginFiles(string source, string destination)
+ internal static void SynchronizePluginFiles(string source, string destination)
{
Directory.CreateDirectory(destination);
- foreach (var file in Directory.GetFiles(source))
+ var sourceFiles = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ var sourceDirectories = new HashSet(StringComparer.OrdinalIgnoreCase);
+ if (Directory.Exists(source))
+ {
+ CollectSourceEntries(
+ source,
+ source,
+ isRoot: true,
+ sourceFiles,
+ sourceDirectories);
+ }
+
+ foreach (var relativeDirectory in sourceDirectories.OrderBy(GetPathDepth))
{
- var fileName = Path.GetFileName(file);
- if (IsManagementFile(fileName))
+ var destinationDirectory = Path.Combine(destination, relativeDirectory);
+ if (File.Exists(destinationDirectory))
+ {
+ File.Delete(destinationDirectory);
+ }
+
+ Directory.CreateDirectory(destinationDirectory);
+ }
+
+ foreach (var (relativePath, sourceFile) in sourceFiles)
+ {
+ var destinationFile = Path.Combine(destination, relativePath);
+ if (FilesMatch(sourceFile, destinationFile))
{
continue;
}
- var destinationFile = Path.Combine(destination, fileName);
- if (!File.Exists(destinationFile))
+ if (Directory.Exists(destinationFile))
{
- File.Copy(file, destinationFile);
+ Directory.Delete(destinationFile, recursive: true);
}
+
+ Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!);
+ CopyFile(sourceFile, destinationFile);
}
- foreach (var subDir in Directory.GetDirectories(source))
+ foreach (var destinationFile in Directory.EnumerateFiles(
+ destination,
+ "*",
+ SearchOption.AllDirectories))
{
- var directoryName = Path.GetFileName(subDir);
- if (IsWorkingDirectory(directoryName))
+ var relativePath = Path.GetRelativePath(destination, destinationFile);
+ if (!sourceFiles.ContainsKey(relativePath))
{
- continue;
+ File.Delete(destinationFile);
}
+ }
- CopyDirectory(subDir, Path.Combine(destination, directoryName));
+ foreach (var destinationDirectory in Directory
+ .EnumerateDirectories(destination, "*", SearchOption.AllDirectories)
+ .OrderByDescending(GetPathDepth))
+ {
+ var relativePath = Path.GetRelativePath(destination, destinationDirectory);
+ if (!sourceDirectories.Contains(relativePath))
+ {
+ Directory.Delete(destinationDirectory, recursive: true);
+ }
}
}
private static bool IsWorkingDirectory(string directoryName)
=> directoryName.EndsWith(".backup", StringComparison.OrdinalIgnoreCase)
|| directoryName.Contains(".backup-", StringComparison.OrdinalIgnoreCase)
+ || directoryName.Contains(".uninstalling-", StringComparison.OrdinalIgnoreCase)
|| directoryName.Contains(".installing-", StringComparison.OrdinalIgnoreCase);
private static bool IsManagementFile(string fileName)
=> fileName.Equals("nuget-manifest.json", StringComparison.OrdinalIgnoreCase)
- || fileName.StartsWith("nuget-manifest.json.tmp-", StringComparison.OrdinalIgnoreCase)
- || fileName.EndsWith(".pending-delete", StringComparison.OrdinalIgnoreCase)
- || fileName.Contains(".pending-delete.tmp-", StringComparison.OrdinalIgnoreCase);
-
- private static void CopyDirectory(string source, string destination)
+ || fileName.StartsWith("nuget-manifest.json.tmp-", StringComparison.OrdinalIgnoreCase);
+
+ private static void CollectSourceEntries(
+ string sourceRoot,
+ string currentDirectory,
+ bool isRoot,
+ Dictionary sourceFiles,
+ HashSet sourceDirectories)
{
- Directory.CreateDirectory(destination);
- foreach (var file in Directory.GetFiles(source))
+ foreach (var file in Directory.EnumerateFiles(currentDirectory))
{
- var destinationFile = Path.Combine(destination, Path.GetFileName(file));
- if (!File.Exists(destinationFile))
+ if (isRoot && IsManagementFile(Path.GetFileName(file)))
{
- File.Copy(file, destinationFile);
+ continue;
}
+
+ sourceFiles[Path.GetRelativePath(sourceRoot, file)] = file;
+ }
+
+ foreach (var subDirectory in Directory.EnumerateDirectories(currentDirectory))
+ {
+ if (isRoot && IsWorkingDirectory(Path.GetFileName(subDirectory)))
+ {
+ continue;
+ }
+
+ var relativePath = Path.GetRelativePath(sourceRoot, subDirectory);
+ sourceDirectories.Add(relativePath);
+ CollectSourceEntries(
+ sourceRoot,
+ subDirectory,
+ isRoot: false,
+ sourceFiles,
+ sourceDirectories);
+ }
+ }
+
+ private static bool FilesMatch(string source, string destination)
+ {
+ if (!File.Exists(destination))
+ {
+ return false;
}
- foreach (var subDir in Directory.GetDirectories(source))
+
+ var sourceInfo = new FileInfo(source);
+ var destinationInfo = new FileInfo(destination);
+ return sourceInfo.Length == destinationInfo.Length
+ && sourceInfo.LastWriteTimeUtc == destinationInfo.LastWriteTimeUtc
+ && sourceInfo.CreationTimeUtc == destinationInfo.CreationTimeUtc;
+ }
+
+ private static void CopyFile(string source, string destination)
+ {
+ var temporaryPath = $"{destination}.sync-{Guid.NewGuid():N}";
+ try
{
- CopyDirectory(subDir, Path.Combine(destination, Path.GetFileName(subDir)));
+ File.Copy(source, temporaryPath);
+ File.SetCreationTimeUtc(temporaryPath, File.GetCreationTimeUtc(source));
+ File.SetLastWriteTimeUtc(temporaryPath, File.GetLastWriteTimeUtc(source));
+ File.Move(temporaryPath, destination, overwrite: true);
+ }
+ finally
+ {
+ if (File.Exists(temporaryPath))
+ {
+ File.Delete(temporaryPath);
+ }
}
}
+
+ private static int GetPathDepth(string path)
+ => path.Count(character =>
+ character == Path.DirectorySeparatorChar
+ || character == Path.AltDirectorySeparatorChar);
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 80044091..1d8b08ba 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -1,5 +1,6 @@
using System.IO;
using System.Net.Http;
+using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
@@ -14,41 +15,6 @@ public sealed class NuGetPluginService : IDisposable
{
private const string NuGetServiceIndexUrl = "https://api.nuget.org/v3/index.json";
private const string PluginTag = "windowtranslator-plugin";
- private const string NuGetFlatContainerBase = "https://api.nuget.org/v3-flatcontainer";
-
- ///
- /// モジュール/パラメータクラス名からNuGetパッケージIDへのマッピング。
- /// アプリバンドルから除外されたプラグインの後方互換性自動インストールに使用します。
- ///
- public static readonly IReadOnlyDictionary KnownClassToPackage =
- new Dictionary(StringComparer.OrdinalIgnoreCase)
- {
- // WindowTranslator.Plugin.FoMPlugin
- ["FoMFilterModule"] = "WindowTranslator.Plugin.FoMPlugin",
- ["FoMOptions"] = "WindowTranslator.Plugin.FoMPlugin",
- // WindowTranslator.Plugin.PLaMoPlugin
- ["PLaMoTranslator"] = "WindowTranslator.Plugin.PLaMoPlugin",
- ["PLaMoOptions"] = "WindowTranslator.Plugin.PLaMoPlugin",
- // WindowTranslator.Plugin.GitHubCopilotPlugin
- ["GitHubCopilotTranslator"] = "WindowTranslator.Plugin.GitHubCopilotPlugin",
- ["GitHubCopilotOptions"] = "WindowTranslator.Plugin.GitHubCopilotPlugin",
- // WindowTranslator.Plugin.DeepLTranslatePlugin
- ["DeepLTranslator"] = "WindowTranslator.Plugin.DeepLTranslatePlugin",
- ["DeepLOptions"] = "WindowTranslator.Plugin.DeepLTranslatePlugin",
- // WindowTranslator.Plugin.GoogleAIPlugin
- ["GoogleAITranslator"] = "WindowTranslator.Plugin.GoogleAIPlugin",
- ["GoogleAIOcr"] = "WindowTranslator.Plugin.GoogleAIPlugin",
- ["GoogleAIOptions"] = "WindowTranslator.Plugin.GoogleAIPlugin",
- // WindowTranslator.Plugin.GoogleAppsSctiptPlugin
- ["GasTranslator"] = "WindowTranslator.Plugin.GoogleAppsSctiptPlugin",
- ["GasOptions"] = "WindowTranslator.Plugin.GoogleAppsSctiptPlugin",
- // WindowTranslator.Plugin.LLMPlugin
- ["LLMTranslator"] = "WindowTranslator.Plugin.LLMPlugin",
- ["LLMOcr"] = "WindowTranslator.Plugin.LLMPlugin",
- ["LLMOptions"] = "WindowTranslator.Plugin.LLMPlugin",
- // WindowTranslator.Plugin.TesseractOCRPlugin
- ["TesseractOcr"] = "WindowTranslator.Plugin.TesseractOCRPlugin",
- };
private static readonly JsonSerializerOptions JsonOptions = new()
{
@@ -63,6 +29,7 @@ public sealed class NuGetPluginService : IDisposable
private readonly string userPluginsDir;
private readonly string manifestPath;
private readonly bool ownsHttpClient;
+ private readonly IReadOnlyDictionary hostPackageVersions;
private readonly SemaphoreSlim operationLock = new(1, 1);
private string? searchUrl;
@@ -79,135 +46,15 @@ internal NuGetPluginService(
ILogger logger,
HttpClient httpClient,
string userPluginsDir,
- bool ownsHttpClient = false)
+ bool ownsHttpClient = false,
+ IReadOnlyDictionary? hostPackageVersions = null)
{
this.logger = logger;
this.httpClient = httpClient;
this.userPluginsDir = Path.GetFullPath(userPluginsDir);
this.manifestPath = Path.Combine(this.userPluginsDir, "nuget-manifest.json");
this.ownsHttpClient = ownsHttpClient;
- }
-
- ///
- /// 指定したパッケージの最新バージョンをインストールします。
- ///
- public async Task InstallLatestPackageAsync(string packageId, IProgress? progress = null, CancellationToken cancellationToken = default)
- {
- var versionsUrl = $"{NuGetFlatContainerBase}/{packageId.ToLowerInvariant()}/index.json";
- using var response = await this.httpClient.GetAsync(versionsUrl, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var versions = await JsonSerializer.DeserializeAsync(content, JsonOptions, cancellationToken).ConfigureAwait(false);
- var parsedVersions = versions?.Versions?
- .Select(NuGetVersion.Parse)
- .OrderByDescending(v => v)
- .ToArray() ?? [];
- var latestVersion = parsedVersions.FirstOrDefault(v => !v.IsPrerelease)
- ?? parsedVersions.FirstOrDefault()
- ?? throw new InvalidOperationException($"パッケージ {packageId} のバージョン一覧を取得できませんでした。");
- await InstallPackageAsync(
- packageId,
- latestVersion.ToNormalizedString(),
- progress,
- cancellationToken).ConfigureAwait(false);
- }
-
- ///
- /// 設定ファイルで参照されているがインストールされていないプラグインを自動インストールします。
- /// アプリバンドルから除外されたプラグインの後方互換性維持のために使用します。
- ///
- public async Task AutoInstallFromSettingsAsync(string settingsPath, CancellationToken cancellationToken = default)
- {
- if (!File.Exists(settingsPath))
- {
- return;
- }
-
- try
- {
- using var doc = JsonDocument.Parse(await File.ReadAllTextAsync(settingsPath, cancellationToken).ConfigureAwait(false));
- var neededPackages = new HashSet(StringComparer.OrdinalIgnoreCase);
-
- if (doc.RootElement.TryGetProperty("Targets", out var targets))
- {
- foreach (var target in targets.EnumerateObject())
- {
- // SelectedPlugins の値(モジュールクラス名)をチェック
- if (target.Value.TryGetProperty("SelectedPlugins", out var selectedPlugins))
- {
- foreach (var plugin in selectedPlugins.EnumerateObject())
- {
- var className = plugin.Value.GetString();
- if (className is not null && KnownClassToPackage.TryGetValue(className, out var packageId))
- {
- neededPackages.Add(packageId);
- }
- }
- }
-
- // PluginParams のキー(パラメータクラス名)をチェック
- if (target.Value.TryGetProperty("PluginParams", out var pluginParams))
- {
- foreach (var param in pluginParams.EnumerateObject())
- {
- if (KnownClassToPackage.TryGetValue(param.Name, out var packageId))
- {
- neededPackages.Add(packageId);
- }
- }
- }
- }
- }
-
- if (neededPackages.Count == 0)
- {
- return;
- }
-
- var installed = await GetInstalledPackagesAsync(cancellationToken).ConfigureAwait(false);
- var installedIds = installed.Select(p => p.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
-
- foreach (var packageId in neededPackages.Where(id => !installedIds.Contains(id)))
- {
- this.logger.LogInformation("設定で参照されているプラグインを自動インストール: {PackageId}", packageId);
- try
- {
- await InstallLatestPackageAsync(packageId, cancellationToken: cancellationToken).ConfigureAwait(false);
- }
- catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
- {
- throw;
- }
- catch (HttpRequestException ex)
- {
- this.logger.LogWarning(
- ex,
- "NuGetへ接続できないため、プラグインの自動インストールを中断します: {PackageId}",
- packageId);
- break;
- }
- catch (TaskCanceledException ex)
- {
- this.logger.LogWarning(
- ex,
- "NuGet接続がタイムアウトしたため、プラグインの自動インストールを中断します: {PackageId}",
- packageId);
- break;
- }
- catch (Exception ex)
- {
- this.logger.LogWarning(ex, "プラグイン {PackageId} の自動インストールに失敗しました。", packageId);
- }
- }
- }
- catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
- {
- throw;
- }
- catch (Exception ex)
- {
- this.logger.LogWarning(ex, "設定からのプラグイン自動インストール処理中にエラーが発生しました。");
- }
+ this.hostPackageVersions = hostPackageVersions ?? CreateHostPackageVersions();
}
///
@@ -252,20 +99,16 @@ public async Task InstallPackageAsync(string packageId, string version, IProgres
var targetDir = GetPackageDirectory(packageId);
var stagingDir = Path.Combine(this.userPluginsDir, $".{packageId}.installing-{operationId}");
var backupDir = $"{targetDir}.backup-{operationId}";
- var pendingDeleteMarker = GetPendingDeleteMarker(packageId);
- var markerWasPresent = false;
- string? markerContent = null;
var targetMoved = false;
var stagingMoved = false;
await this.operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
- markerWasPresent = File.Exists(pendingDeleteMarker);
- markerContent = markerWasPresent
- ? await File.ReadAllTextAsync(pendingDeleteMarker, cancellationToken).ConfigureAwait(false)
- : null;
Directory.CreateDirectory(this.userPluginsDir);
- var installer = new NuGetPackageInstaller(this.httpClient, this.logger);
+ var installer = new NuGetPackageInstaller(
+ this.httpClient,
+ this.logger,
+ this.hostPackageVersions);
await installer.InstallAsync(
packageId,
version,
@@ -276,11 +119,6 @@ await installer.InstallAsync(
var currentManifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
var updatedManifest = AddOrUpdatePackage(currentManifest, packageId, version);
- if (markerWasPresent)
- {
- File.Delete(pendingDeleteMarker);
- }
-
if (Directory.Exists(targetDir))
{
Directory.Move(targetDir, backupDir);
@@ -321,13 +159,6 @@ await installer.InstallAsync(
{
Directory.Move(backupDir, targetDir);
}
- if (markerWasPresent && !File.Exists(pendingDeleteMarker))
- {
- await File.WriteAllTextAsync(
- pendingDeleteMarker,
- markerContent ?? packageId,
- CancellationToken.None).ConfigureAwait(false);
- }
}
catch (Exception rollbackException)
{
@@ -346,86 +177,73 @@ await File.WriteAllTextAsync(
}
///
- /// 指定したパッケージをアンインストールします。(次回起動時に適用)
+ /// 指定したパッケージを管理フォルダから削除します。
+ /// 実行中のプラグインは一時フォルダから読み込まれているため、反映には再起動が必要です。
///
public async Task UninstallPackageAsync(string packageId, CancellationToken cancellationToken = default)
{
+ var operationId = Guid.NewGuid().ToString("N");
+ var targetDir = GetPackageDirectory(packageId);
+ var uninstallingDir = $"{targetDir}.uninstalling-{operationId}";
+ var targetMoved = false;
await this.operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
this.logger.LogInformation("パッケージをアンインストール: {PackageId}", packageId);
- _ = GetPackageDirectory(packageId);
Directory.CreateDirectory(this.userPluginsDir);
- var pendingDeleteMarker = GetPendingDeleteMarker(packageId);
- var markerAlreadyExisted = File.Exists(pendingDeleteMarker);
var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
var updatedManifest = RemovePackage(manifest, packageId);
+ if (Directory.Exists(targetDir))
+ {
+ Directory.Move(targetDir, uninstallingDir);
+ targetMoved = true;
+ }
+
try
{
- await WriteTextAtomicallyAsync(
- pendingDeleteMarker,
- packageId,
- cancellationToken).ConfigureAwait(false);
await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
}
catch
{
- if (!markerAlreadyExisted)
+ try
{
- TryDeleteFile(pendingDeleteMarker);
+ if (targetMoved
+ && Directory.Exists(uninstallingDir)
+ && !Directory.Exists(targetDir))
+ {
+ Directory.Move(uninstallingDir, targetDir);
+ }
+ }
+ catch (Exception rollbackException)
+ {
+ this.logger.LogError(
+ rollbackException,
+ "プラグイン {PackageId} のアンインストール失敗後の復旧に失敗しました。",
+ packageId);
}
throw;
}
- this.logger.LogInformation(
- "パッケージ {PackageId} をアンインストールキューに追加しました。再起動後に完全に削除されます。",
- packageId);
- }
- finally
- {
- this.operationLock.Release();
- }
- }
-
- ///
- /// アプリ起動時にペンディング削除マーカーを処理します。
- ///
- public void ProcessPendingDeletions()
- {
- this.operationLock.Wait();
- try
- {
- if (!Directory.Exists(this.userPluginsDir))
- {
- return;
- }
-
- foreach (var markerFile in Directory.GetFiles(this.userPluginsDir, "*.pending-delete"))
+ try
{
- try
- {
- var packageId = File.ReadAllText(markerFile);
- var markerPackageId = Path.GetFileName(markerFile)[..^".pending-delete".Length];
- if (!packageId.Equals(markerPackageId, StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException("削除マーカーのパッケージIDがファイル名と一致しません。");
- }
-
- var targetDir = GetPackageDirectory(packageId);
- if (Directory.Exists(targetDir))
- {
- Directory.Delete(targetDir, recursive: true);
- this.logger.LogInformation("ペンディング削除を処理: {PackageId}", packageId);
- }
- File.Delete(markerFile);
- }
- catch (Exception ex)
+ if (Directory.Exists(uninstallingDir))
{
- this.logger.LogWarning(ex, "ペンディング削除の処理に失敗: {MarkerFile}", markerFile);
+ Directory.Delete(uninstallingDir, recursive: true);
}
}
+ catch (Exception ex)
+ {
+ this.logger.LogWarning(
+ ex,
+ "アンインストール済みプラグインフォルダの削除に失敗しました: {Directory}",
+ uninstallingDir);
+ }
+
+ this.logger.LogInformation(
+ "パッケージ {PackageId} を管理フォルダからアンインストールしました。再起動後に反映されます。",
+ packageId);
}
finally
{
@@ -464,7 +282,7 @@ private static InstalledManifest AddOrUpdatePackage(
{
var packages = manifest.Packages.ToList();
var existing = packages.FindIndex(p => p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase));
- var newEntry = new InstalledPackageInfo(packageId, version, DateTime.UtcNow);
+ var newEntry = new InstalledPackageInfo(packageId, version);
if (existing >= 0)
{
packages[existing] = newEntry;
@@ -477,6 +295,34 @@ private static InstalledManifest AddOrUpdatePackage(
return new InstalledManifest([.. packages]);
}
+ private static Dictionary CreateHostPackageVersions()
+ {
+ var abstractionsAssembly = typeof(UserSettings).Assembly;
+ var informationalVersion = abstractionsAssembly
+ .GetCustomAttribute()?
+ .InformationalVersion;
+ if (!string.IsNullOrWhiteSpace(informationalVersion)
+ && NuGetVersion.TryParse(informationalVersion, out var packageVersion))
+ {
+ return new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["WindowTranslator.Abstractions"] = packageVersion,
+ };
+ }
+
+ var assemblyVersion = abstractionsAssembly.GetName().Version
+ ?? throw new InvalidOperationException(
+ "WindowTranslator.Abstractions のバージョンを取得できませんでした。");
+ var fallbackVersion = new NuGetVersion(
+ assemblyVersion.Major,
+ assemblyVersion.Minor,
+ Math.Max(assemblyVersion.Build, 0));
+ return new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["WindowTranslator.Abstractions"] = fallbackVersion,
+ };
+ }
+
private static InstalledManifest RemovePackage(InstalledManifest manifest, string packageId)
=> new([.. manifest.Packages.Where(p =>
!p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase))]);
@@ -491,11 +337,18 @@ private async Task LoadManifestAsync(CancellationToken cancel
try
{
await using var fs = File.OpenRead(this.manifestPath);
- return await JsonSerializer.DeserializeAsync(
+ var manifest = await JsonSerializer.DeserializeAsync(
fs,
JsonOptions,
cancellationToken).ConfigureAwait(false)
?? throw new InvalidDataException("プラグインマニフェストが空です。");
+ if (manifest.Packages is null)
+ {
+ throw new InvalidDataException(
+ "プラグインマニフェストにインストール済みパッケージ一覧がありません。");
+ }
+
+ return manifest;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
@@ -530,23 +383,6 @@ private async Task SaveManifestAsync(InstalledManifest manifest, CancellationTok
}
}
- private static async Task WriteTextAtomicallyAsync(
- string destinationPath,
- string content,
- CancellationToken cancellationToken)
- {
- var temporaryPath = $"{destinationPath}.tmp-{Guid.NewGuid():N}";
- try
- {
- await File.WriteAllTextAsync(temporaryPath, content, cancellationToken).ConfigureAwait(false);
- ReplaceFile(temporaryPath, destinationPath);
- }
- finally
- {
- TryDeleteFile(temporaryPath);
- }
- }
-
private static void ReplaceFile(string sourcePath, string destinationPath)
=> File.Move(sourcePath, destinationPath, overwrite: true);
@@ -573,12 +409,6 @@ private string GetPackageDirectory(string packageId)
return packageDirectory;
}
- private string GetPendingDeleteMarker(string packageId)
- {
- _ = GetPackageDirectory(packageId);
- return Path.Combine(this.userPluginsDir, $"{packageId}.pending-delete");
- }
-
private static void TryDeleteDirectory(string directory)
{
try
@@ -633,11 +463,10 @@ public record NuGetPackageInfo(
/// インストール済みパッケージ情報
public record InstalledPackageInfo(
string Id,
- string Version,
- DateTime InstalledAt
+ string Version
);
-/// インストール済みパッケージのマニフェスト
+/// NuGetプラグインの管理マニフェスト
public record InstalledManifest(List Packages);
// NuGet V3 API レスポンス型
@@ -664,7 +493,3 @@ internal record NuGetSearchData(
[property: JsonPropertyName("projectUrl")] string? ProjectUrl,
[property: JsonPropertyName("licenseUrl")] string? LicenseUrl
);
-
-internal record NuGetVersionListResponse(
- [property: JsonPropertyName("versions")] string[]? Versions
-);
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index 8059f580..649d9f2c 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -17,7 +17,6 @@ public partial class PluginStoreViewModel : ObservableObject
private readonly NuGetPluginService nugetService;
private readonly ILogger logger;
private readonly IContentDialogService dialogService;
- private readonly ISnackbarService snackbarService;
[ObservableProperty]
private bool isLoading;
@@ -33,13 +32,11 @@ public partial class PluginStoreViewModel : ObservableObject
public PluginStoreViewModel(
NuGetPluginService nugetService,
ILogger logger,
- IContentDialogService dialogService,
- ISnackbarService snackbarService)
+ IContentDialogService dialogService)
{
this.nugetService = nugetService;
this.logger = logger;
this.dialogService = dialogService;
- this.snackbarService = snackbarService;
}
///
@@ -88,7 +85,7 @@ public async Task LoadAsync(CancellationToken cancellationToken = default)
}
}
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// キャンセルは正常
}
@@ -107,14 +104,20 @@ public async Task LoadAsync(CancellationToken cancellationToken = default)
/// プラグインをインストールまたは更新します。
///
[RelayCommand]
- public async Task InstallAsync(PluginPackageViewModel package)
+ public async Task InstallAsync(
+ PluginPackageViewModel package,
+ CancellationToken cancellationToken = default)
{
package.IsInstalling = true;
try
{
this.logger.LogInformation("プラグインのインストール開始: {PackageId} {Version}", package.Id, package.LatestVersion);
var progress = new Progress(v => package.InstallProgress = v);
- await this.nugetService.InstallPackageAsync(package.Id, package.LatestVersion, progress).ConfigureAwait(true);
+ await this.nugetService.InstallPackageAsync(
+ package.Id,
+ package.LatestVersion,
+ progress,
+ cancellationToken).ConfigureAwait(true);
package.IsInstalled = true;
package.InstalledVersion = package.LatestVersion;
@@ -129,9 +132,9 @@ await this.dialogService.ShowSimpleDialogAsync(new()
Title = Resources.PluginInstallSuccess,
Content = Resources.RestartRequired,
CloseButtonText = Resources.Close,
- }).ConfigureAwait(true);
+ }, cancellationToken).ConfigureAwait(true);
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// キャンセルは正常
}
@@ -141,7 +144,8 @@ await this.dialogService.ShowSimpleDialogAsync(new()
await this.dialogService.ShowAlertAsync(
Resources.PluginInstallFailed,
ex.Message,
- Resources.Close).ConfigureAwait(true);
+ Resources.Close,
+ cancellationToken).ConfigureAwait(true);
}
finally
{
diff --git a/WindowTranslator/Program.cs b/WindowTranslator/Program.cs
index 59274802..db70867c 100644
--- a/WindowTranslator/Program.cs
+++ b/WindowTranslator/Program.cs
@@ -54,14 +54,6 @@
var exeDir = Path.GetDirectoryName(Environment.GetCommandLineArgs()[0])!;
Directory.SetCurrentDirectory(exeDir);
-// ペンディング削除処理と設定参照プラグインの自動インストール(カタログ初期化より前に実行する必要がある)
-{
- using var earlyLoggerFactory = LoggerFactory.Create(b => b.SetMinimumLevel(LogLevel.Warning));
- using var earlyNuGetService = new NuGetPluginService(earlyLoggerFactory.CreateLogger());
- earlyNuGetService.ProcessPendingDeletions();
- await earlyNuGetService.AutoInstallFromSettingsAsync(PathUtility.UserSettings);
-}
-
var builder = KamishibaiApplication.CreateBuilder();
builder.Host.ConfigureLogging((c, l) =>
@@ -168,7 +160,7 @@
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddTransient();
-builder.Services.Configure(builder.Configuration, op => op.ErrorOnUnknownConfiguration = false);
+builder.Services.AddTransient, ConfigureUserSettings>();
builder.Services.Configure(builder.Configuration.GetSection(nameof(UserSettings.Common)));
builder.Services.AddTransient(typeof(IConfigureNamedOptions<>), typeof(ConfigurePluginParam<>));
builder.Services.AddTransient(typeof(IConfigureOptions<>), typeof(ConfigurePluginParam<>));
@@ -236,11 +228,23 @@ static string GetPluginName(PluginNameOptions options, Type type)
}
}
-class ConfigurePluginParam(IConfiguration configuration, IProcessInfoStore store) : IConfigureNamedOptions
+class ConfigureUserSettings(IConfiguration configuration) : IConfigureOptions
+{
+ private readonly IConfiguration configuration = configuration;
+
+ public void Configure(UserSettings options)
+ => PluginParameterIgnoringConfigurationBinder.Bind(this.configuration, options);
+}
+
+class ConfigurePluginParam(
+ IConfiguration configuration,
+ IProcessInfoStore store,
+ ILogger> logger) : IConfigureNamedOptions
where TOptions : class, IPluginParam
{
private readonly IConfiguration configuration = configuration.GetSection(nameof(UserSettings.Targets));
private readonly IProcessInfoStore store = store;
+ private readonly ILogger> logger = logger;
public void Configure(TOptions options)
{
@@ -249,7 +253,7 @@ public void Configure(TOptions options)
{
section = this.configuration.GetSection(Options.DefaultName);
}
- GetTargetSection(section, typeof(TOptions).Name).Bind(options);
+ this.BindParameter(section, options);
}
public void Configure(string? name, TOptions options)
@@ -260,7 +264,46 @@ public void Configure(string? name, TOptions options)
{
section = this.configuration.GetSection(Options.DefaultName);
}
- GetTargetSection(section, typeof(TOptions).Name).Bind(options);
+ this.BindParameter(section, options);
+ }
+
+ private void BindParameter(IConfigurationSection targetSection, TOptions options)
+ {
+ var parameterSection = GetTargetSection(targetSection, typeof(TOptions).Name);
+ if (!parameterSection.Exists())
+ {
+ return;
+ }
+
+ try
+ {
+ var configured = parameterSection.Get();
+ if (configured is null)
+ {
+ return;
+ }
+
+ foreach (var property in typeof(TOptions).GetProperties(BindingFlags.Instance | BindingFlags.Public))
+ {
+ if (property.CanRead
+ && property.CanWrite
+ && property.GetIndexParameters().Length == 0)
+ {
+ property.SetValue(options, property.GetValue(configured));
+ }
+ }
+ }
+ catch (Exception ex) when (ex is InvalidOperationException
+ or FormatException
+ or NotSupportedException
+ or MissingMethodException
+ or ArgumentException
+ or TargetInvocationException)
+ {
+ this.logger.LogWarning(
+ "プラグインパラメータ {ParameterType} を読み込めないため無視します。",
+ typeof(TOptions).Name);
+ }
}
private static IConfigurationSection GetTargetSection(IConfigurationSection section, string name)
@@ -288,7 +331,7 @@ public void Configure(TargetSettings options)
{
section = this.configuration.GetSection(Options.DefaultName);
}
- section.Bind(options);
+ PluginParameterIgnoringConfigurationBinder.Bind(section, options);
}
public void Configure(string? name, TargetSettings options)
@@ -299,7 +342,25 @@ public void Configure(string? name, TargetSettings options)
{
section = this.configuration.GetSection(Options.DefaultName);
}
- section.Bind(options);
+ PluginParameterIgnoringConfigurationBinder.Bind(section, options);
+ }
+}
+
+static class PluginParameterIgnoringConfigurationBinder
+{
+ public static void Bind(IConfiguration configuration, object options)
+ {
+ var values = configuration
+ .AsEnumerable(makePathsRelative: true)
+ .Where(value => !value.Key
+ .Split(ConfigurationPath.KeyDelimiter, StringSplitOptions.None)
+ .Contains(
+ nameof(TargetSettings.PluginParams),
+ StringComparer.OrdinalIgnoreCase));
+ var filteredConfiguration = new ConfigurationBuilder()
+ .AddInMemoryCollection(values)
+ .Build();
+ filteredConfiguration.Bind(options);
}
}
diff --git a/WindowTranslator/Properties/Resources.Designer.cs b/WindowTranslator/Properties/Resources.Designer.cs
index 9e1518db..dd5417ea 100644
--- a/WindowTranslator/Properties/Resources.Designer.cs
+++ b/WindowTranslator/Properties/Resources.Designer.cs
@@ -1,4 +1,4 @@
-//------------------------------------------------------------------------------
+//------------------------------------------------------------------------------
//
// This code was generated by a tool.
//
@@ -28,7 +28,7 @@
namespace WindowTranslator.Properties;
///
-/// ローカライズされた文字列などを検索するための、厳密に型指定されたリソース クラスです。
+/// [JCYꂽȂǂ邽߂́AɌ^w肳ꂽ\[X NXłB
///
// This class was auto-generated by a text template.
// To add or remove a member, edit your .ResX file.
@@ -45,15 +45,15 @@ internal Resources() {
}
///
- /// このクラスで使用されているキャッシュされた ResourceManager インスタンスを返します。
+ /// ̃NXŎgpĂLbVꂽ ResourceManager CX^XԂ܂B
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager
=> resourceMan ??= new CustomResourceManager("WindowTranslator.Properties.Resources", Assembly.GetExecutingAssembly());
///
- /// すべてについて、現在のスレッドの CurrentUICulture プロパティをオーバーライドします
- /// 現在のスレッドの CurrentUICulture プロパティをオーバーライドします。
+ /// ׂĂɂāÃ݂Xbh CurrentUICulture vpeBI[o[Ch܂
+ /// ݂̃Xbh CurrentUICulture vpeBI[o[Ch܂B
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture
@@ -63,622 +63,622 @@ internal Resources() {
}
///
- /// "このアプリについて" に類似しているローカライズされた文字列を検索します。
+ /// "̃Avɂ" ɗގĂ郍[JCYꂽ܂B
///
public static string About => ResourceManager.GetString("About", resourceCulture) ?? string.Empty;
///
- /// "連絡先" に類似しているローカライズされた文字列を検索します。
+ /// "A" ɗގĂ郍[JCYꂽ܂B
///
public static string Address => ResourceManager.GetString("Address", resourceCulture) ?? string.Empty;
///
- /// "アプリ情報" に類似しているローカライズされた文字列を検索します。
+ /// "Av" ɗގĂ郍[JCYꂽ܂B
///
public static string Application => ResourceManager.GetString("Application", resourceCulture) ?? string.Empty;
///
- /// "適用" に類似しているローカライズされた文字列を検索します。
+ /// "Kp" ɗގĂ郍[JCYꂽ܂B
///
public static string Apply => ResourceManager.GetString("Apply", resourceCulture) ?? string.Empty;
///
- /// "アタッチ" に類似しているローカライズされた文字列を検索します。
+ /// "A^b`" ɗގĂ郍[JCYꂽ܂B
///
public static string Attach => ResourceManager.GetString("Attach", resourceCulture) ?? string.Empty;
///
- /// "アタッチ中" に類似しているローカライズされた文字列を検索します。
+ /// "A^b`" ɗގĂ郍[JCYꂽ܂B
///
public static string Attaching => ResourceManager.GetString("Attaching", resourceCulture) ?? string.Empty;
///
- /// "自動スクロール" に類似しているローカライズされた文字列を検索します。
+ /// "XN[" ɗގĂ郍[JCYꂽ܂B
///
public static string AutoScroll => ResourceManager.GetString("AutoScroll", resourceCulture) ?? string.Empty;
///
- /// "自動起動" に類似しているローカライズされた文字列を検索します。
+ /// "N" ɗގĂ郍[JCYꂽ܂B
///
public static string AutoStart => ResourceManager.GetString("AutoStart", resourceCulture) ?? string.Empty;
///
- /// "PC起動時に自動起動" に類似しているローカライズされた文字列を検索します。
+ /// "PCNɎN" ɗގĂ郍[JCYꂽ܂B
///
public static string AutoStartWithPC => ResourceManager.GetString("AutoStartWithPC", resourceCulture) ?? string.Empty;
///
- /// "自動翻訳対象" に類似しているローカライズされた文字列を検索します。
+ /// "|Ώ" ɗގĂ郍[JCYꂽ܂B
///
public static string AutoTargets => ResourceManager.GetString("AutoTargets", resourceCulture) ?? string.Empty;
///
- /// "ビルド日時" に類似しているローカライズされた文字列を検索します。
+ /// "rh" ɗގĂ郍[JCYꂽ܂B
///
public static string BuildDate => ResourceManager.GetString("BuildDate", resourceCulture) ?? string.Empty;
///
- /// "キャッシュモジュール" に類似しているローカライズされた文字列を検索します。
+ /// "LbVW[" ɗގĂ郍[JCYꂽ܂B
///
public static string CacheModule => ResourceManager.GetString("CacheModule", resourceCulture) ?? string.Empty;
///
- /// "キャッシュ設定" に類似しているローカライズされた文字列を検索します。
+ /// "LbVݒ" ɗގĂ郍[JCYꂽ܂B
///
public static string CacheParam => ResourceManager.GetString("CacheParam", resourceCulture) ?? string.Empty;
///
- /// "キャンセル" に類似しているローカライズされた文字列を検索します。
+ /// "LZ" ɗގĂ郍[JCYꂽ܂B
///
public static string Cancel => ResourceManager.GetString("Cancel", resourceCulture) ?? string.Empty;
///
- /// "キャプチャーウィンドウ" に類似しているローカライズされた文字列を検索します。
+ /// "Lv`[EBhE" ɗގĂ郍[JCYꂽ܂B
///
public static string Capture => ResourceManager.GetString("Capture", resourceCulture) ?? string.Empty;
///
- /// "新しいバージョンのチェック" に類似しているローカライズされた文字列を検索します。
+ /// "Vo[W̃`FbN" ɗގĂ郍[JCYꂽ܂B
///
public static string CheckNewVersion => ResourceManager.GetString("CheckNewVersion", resourceCulture) ?? string.Empty;
///
- /// "更新内容の確認" に類似しているローカライズされた文字列を検索します。
+ /// "XVe̊mF" ɗގĂ郍[JCYꂽ܂B
///
public static string CheckUpdateNotes => ResourceManager.GetString("CheckUpdateNotes", resourceCulture) ?? string.Empty;
///
- /// "クリア" に類似しているローカライズされた文字列を検索します。
+ /// "NA" ɗގĂ郍[JCYꂽ܂B
///
public static string Clear => ResourceManager.GetString("Clear", resourceCulture) ?? string.Empty;
///
- /// "閉じる" に類似しているローカライズされた文字列を検索します。
+ /// "" ɗގĂ郍[JCYꂽ܂B
///
public static string Close => ResourceManager.GetString("Close", resourceCulture) ?? string.Empty;
///
- /// "確認" に類似しているローカライズされた文字列を検索します。
+ /// "mF" ɗގĂ郍[JCYꂽ܂B
///
public static string Confirm => ResourceManager.GetString("Confirm", resourceCulture) ?? string.Empty;
///
- /// "コピーしました" に類似しているローカライズされた文字列を検索します。
+ /// "Rs[܂" ɗގĂ郍[JCYꂽ܂B
///
public static string Copied => ResourceManager.GetString("Copied", resourceCulture) ?? string.Empty;
///
- /// "情報をコピー" に類似しているローカライズされた文字列を検索します。
+ /// "Rs[" ɗގĂ郍[JCYꂽ܂B
///
public static string Copy => ResourceManager.GetString("Copy", resourceCulture) ?? string.Empty;
///
- /// "デフォルト設定" に類似しているローカライズされた文字列を検索します。
+ /// "ftHgݒ" ɗގĂ郍[JCYꂽ܂B
///
public static string DefaultSetting => ResourceManager.GetString("DefaultSetting", resourceCulture) ?? string.Empty;
///
- /// "デタッチ" に類似しているローカライズされた文字列を検索します。
+ /// "f^b`" ɗގĂ郍[JCYꂽ܂B
///
public static string Detach => ResourceManager.GetString("Detach", resourceCulture) ?? string.Empty;
///
- /// "技術情報" に類似しているローカライズされた文字列を検索します。
+ /// "Zp" ɗގĂ郍[JCYꂽ܂B
///
public static string Develop => ResourceManager.GetString("Develop", resourceCulture) ?? string.Empty;
///
- /// "開発者" に類似しているローカライズされた文字列を検索します。
+ /// "J" ɗގĂ郍[JCYꂽ܂B
///
public static string DevelopedBy => ResourceManager.GetString("DevelopedBy", resourceCulture) ?? string.Empty;
///
- /// "処理中アイコンを表示する" に類似しているローカライズされた文字列を検索します。
+ /// "ACR\" ɗގĂ郍[JCYꂽ܂B
///
public static string DisplayBusy => ResourceManager.GetString("DisplayBusy", resourceCulture) ?? string.Empty;
///
- /// "表示方法" に類似しているローカライズされた文字列を検索します。
+ /// "\@" ɗގĂ郍[JCYꂽ܂B
///
public static string DisplayMethod => ResourceManager.GetString("DisplayMethod", resourceCulture) ?? string.Empty;
///
- /// "終了" に類似しているローカライズされた文字列を検索します。
+ /// "I" ɗގĂ郍[JCYꂽ܂B
///
public static string Exit => ResourceManager.GetString("Exit", resourceCulture) ?? string.Empty;
///
- /// "エクスポート" に類似しているローカライズされた文字列を検索します。
+ /// "GNX|[g" ɗގĂ郍[JCYꂽ܂B
///
public static string Export => ResourceManager.GetString("Export", resourceCulture) ?? string.Empty;
///
- /// "ログのエクスポート" に類似しているローカライズされた文字列を検索します。
+ /// "ÕGNX|[g" ɗގĂ郍[JCYꂽ܂B
///
public static string ExportLogs => ResourceManager.GetString("ExportLogs", resourceCulture) ?? string.Empty;
///
- /// "エクスポート失敗" に類似しているローカライズされた文字列を検索します。
+ /// "GNX|[gs" ɗގĂ郍[JCYꂽ܂B
///
public static string ExportLogsFailed => ResourceManager.GetString("ExportLogsFailed", resourceCulture) ?? string.Empty;
///
- /// "テキストファイル" に類似しているローカライズされた文字列を検索します。
+ /// "eLXgt@C" ɗގĂ郍[JCYꂽ܂B
///
public static string ExportLogsFilterText => ResourceManager.GetString("ExportLogsFilterText", resourceCulture) ?? string.Empty;
///
- /// "エクスポート完了" に類似しているローカライズされた文字列を検索します。
+ /// "GNX|[g" ɗގĂ郍[JCYꂽ܂B
///
public static string ExportLogsSuccess => ResourceManager.GetString("ExportLogsSuccess", resourceCulture) ?? string.Empty;
///
- /// "ログを`{0}`にエクスポートしました。" に類似しているローカライズされた文字列を検索します。
+ /// "O`{0}`ɃGNX|[g܂B" ɗގĂ郍[JCYꂽ܂B
///
public static string ExportLogsSuccessDetail => ResourceManager.GetString("ExportLogsSuccessDetail", resourceCulture) ?? string.Empty;
///
- /// "設定の適用に失敗しました。" に類似しているローカライズされた文字列を検索します。
+ /// "ݒ̓KpɎs܂B" ɗގĂ郍[JCYꂽ܂B
///
public static string FaildApplySettings => ResourceManager.GetString("FaildApplySettings", resourceCulture) ?? string.Empty;
///
- /// "OCRに失敗しました" に類似しているローカライズされた文字列を検索します。
+ /// "OCRɎs܂" ɗގĂ郍[JCYꂽ܂B
///
public static string FaildOcr => ResourceManager.GetString("FaildOcr", resourceCulture) ?? string.Empty;
///
- /// "ウィンドウの埋め込みに失敗しました。" に類似しているローカライズされた文字列を検索します。
+ /// "EBhE̖ߍ݂Ɏs܂B" ɗގĂ郍[JCYꂽ܂B
///
public static string FaildOverlay => ResourceManager.GetString("FaildOverlay", resourceCulture) ?? string.Empty;
///
- /// "翻訳に失敗しました" に類似しているローカライズされた文字列を検索します。
+ /// "|Ɏs܂" ɗގĂ郍[JCYꂽ܂B
///
public static string FaildTranslate => ResourceManager.GetString("FaildTranslate", resourceCulture) ?? string.Empty;
///
- /// "近いテキストの閾値" に類似しているローカライズされた文字列を検索します。
+ /// "߂eLXg臒l" ɗގĂ郍[JCYꂽ܂B
///
public static string FuzzyMatchThreshold => ResourceManager.GetString("FuzzyMatchThreshold", resourceCulture) ?? string.Empty;
///
- /// "全般設定" に類似しているローカライズされた文字列を検索します。
+ /// "Sʐݒ" ɗގĂ郍[JCYꂽ܂B
///
public static string GeneralSettings => ResourceManager.GetString("GeneralSettings", resourceCulture) ?? string.Empty;
///
- /// "アップデートがあります: {0}" に類似しているローカライズされた文字列を検索します。
+ /// "Abvf[g܂: {0}" ɗގĂ郍[JCYꂽ܂B
///
public static string HasUpdate => ResourceManager.GetString("HasUpdate", resourceCulture) ?? string.Empty;
///
- /// "押している間だけ" に類似しているローカライズされた文字列を検索します。
+ /// "ĂԂ" ɗގĂ郍[JCYꂽ܂B
///
public static string Hold => ResourceManager.GetString("Hold", resourceCulture) ?? string.Empty;
///
- /// "メモリ内キャッシュ" に類似しているローカライズされた文字列を検索します。
+ /// "LbV" ɗގĂ郍[JCYꂽ܂B
///
public static string InMemoryCache => ResourceManager.GetString("InMemoryCache", resourceCulture) ?? string.Empty;
///
- /// "インストール" に類似しているローカライズされた文字列を検索します。
+ /// "CXg[" ɗގĂ郍[JCYꂽ܂B
///
public static string Install => ResourceManager.GetString("Install", resourceCulture) ?? string.Empty;
///
- /// "インストール済み" に類似しているローカライズされた文字列を検索します。
+ /// "CXg[ς" ɗގĂ郍[JCYꂽ܂B
///
public static string Installed => ResourceManager.GetString("Installed", resourceCulture) ?? string.Empty;
///
- /// "インストール済み: {0}" に類似しているローカライズされた文字列を検索します。
+ /// "CXg[ς: {0}" ɗގĂ郍[JCYꂽ܂B
///
public static string InstalledVersion => ResourceManager.GetString("InstalledVersion", resourceCulture) ?? string.Empty;
///
- /// "インストール済みバージョン" に類似しているローカライズされた文字列を検索します。
+ /// "CXg[ς݃o[W" ɗގĂ郍[JCYꂽ܂B
///
public static string InstalledVersionLabel => ResourceManager.GetString("InstalledVersionLabel", resourceCulture) ?? string.Empty;
///
- /// "新しいバージョン: {0} のインストール" に類似しているローカライズされた文字列を検索します。
+ /// "Vo[W: {0} ̃CXg[" ɗގĂ郍[JCYꂽ܂B
///
public static string InstallNewVersion => ResourceManager.GetString("InstallNewVersion", resourceCulture) ?? string.Empty;
///
- /// "{0}: 設定検証エラー" に類似しているローカライズされた文字列を検索します。
+ /// "{0}: ݒ茟G[" ɗގĂ郍[JCYꂽ܂B
///
public static string InvalidSettings => ResourceManager.GetString("InvalidSettings", resourceCulture) ?? string.Empty;
///
- /// ":tired-face: **そのまま実行しても動作しない可能性が高いです** **..." に類似しているローカライズされた文字列を検索します。
+ /// ":tired-face: **̂܂sĂ삵Ȃ\ł** &a..." ɗގĂ郍[JCYꂽ܂B
///
public static string InvalidSettingsContent => ResourceManager.GetString("InvalidSettingsContent", resourceCulture) ?? string.Empty;
///
- /// "一度翻訳対象に選択したプロセスが起動したときに自動的に翻訳する" に類似しているローカライズされた文字列を検索します。
+ /// "x|ΏۂɑIvZXNƂɎIɖ|" ɗގĂ郍[JCYꂽ܂B
///
public static string IsEnableAutoTarget => ResourceManager.GetString("IsEnableAutoTarget", resourceCulture) ?? string.Empty;
///
- /// "オーバーレイ表示をキャプチャー可能にする" に類似しているローカライズされた文字列を検索します。
+ /// "I[o[C\Lv`[\ɂ" ɗގĂ郍[JCYꂽ܂B
///
public static string IsEnableCaptureOverlay => ResourceManager.GetString("IsEnableCaptureOverlay", resourceCulture) ?? string.Empty;
///
- /// "最新バージョンをご利用中です。" に類似しているローカライズされた文字列を検索します。
+ /// "ŐVo[WpłB" ɗގĂ郍[JCYꂽ܂B
///
public static string IsLatest => ResourceManager.GetString("IsLatest", resourceCulture) ?? string.Empty;
///
- /// "マウスポインター位置のテキストのみオーバレイ翻訳を表示する" に類似しているローカライズされた文字列を検索します。
+ /// "}EX|C^[ʒũeLXĝ݃I[oC|\" ɗގĂ郍[JCYꂽ܂B
///
public static string IsOverlayPointSwap => ResourceManager.GetString("IsOverlayPointSwap", resourceCulture) ?? string.Empty;
///
- /// "言語設定" に類似しているローカライズされた文字列を検索します。
+ /// "ݒ" ɗގĂ郍[JCYꂽ܂B
///
public static string Language => ResourceManager.GetString("Language", resourceCulture) ?? string.Empty;
///
- /// "最新バージョン" に類似しているローカライズされた文字列を検索します。
+ /// "ŐVo[W" ɗގĂ郍[JCYꂽ܂B
///
public static string LatestVersion => ResourceManager.GetString("LatestVersion", resourceCulture) ?? string.Empty;
///
- /// "ライセンス" に類似しているローカライズされた文字列を検索します。
+ /// "CZX" ɗގĂ郍[JCYꂽ܂B
///
public static string License => ResourceManager.GetString("License", resourceCulture) ?? string.Empty;
///
- /// "ライセンス情報" に類似しているローカライズされた文字列を検索します。
+ /// "CZX" ɗގĂ郍[JCYꂽ܂B
///
public static string LicenseUrl => ResourceManager.GetString("LicenseUrl", resourceCulture) ?? string.Empty;
///
- /// "ローカルファイルキャッシュ" に類似しているローカライズされた文字列を検索します。
+ /// "[Jt@CLbV" ɗގĂ郍[JCYꂽ܂B
///
public static string LocalCache => ResourceManager.GetString("LocalCache", resourceCulture) ?? string.Empty;
///
- /// "ログ" に類似しているローカライズされた文字列を検索します。
+ /// "O" ɗގĂ郍[JCYꂽ܂B
///
public static string Log => ResourceManager.GetString("Log", resourceCulture) ?? string.Empty;
///
- /// "その他" に類似しているローカライズされた文字列を検索します。
+ /// "̑" ɗގĂ郍[JCYꂽ܂B
///
public static string Misc => ResourceManager.GetString("Misc", resourceCulture) ?? string.Empty;
///
- /// "すでにWindowTranslatorが起動中です" に類似しているローカライズされた文字列を検索します。
+ /// "łWindowTranslatorNł" ɗގĂ郍[JCYꂽ܂B
///
public static string MutexError => ResourceManager.GetString("MutexError", resourceCulture) ?? string.Empty;
///
- /// "新しいバージョン: {0} がリリースされました" に類似しているローカライズされた文字列を検索します。
+ /// "Vo[W: {0} [X܂" ɗގĂ郍[JCYꂽ܂B
///
public static string NewVersionAvailable => ResourceManager.GetString("NewVersionAvailable", resourceCulture) ?? string.Empty;
///
- /// "キャッシュしない" に類似しているローカライズされた文字列を検索します。
+ /// "LbVȂ" ɗގĂ郍[JCYꂽ܂B
///
public static string NoCache => ResourceManager.GetString("NoCache", resourceCulture) ?? string.Empty;
///
- /// "翻訳しない" に類似しているローカライズされた文字列を検索します。
+ /// "|Ȃ" ɗގĂ郍[JCYꂽ܂B
///
public static string NoTranslateModule => ResourceManager.GetString("NoTranslateModule", resourceCulture) ?? string.Empty;
///
- /// "NuGetからのプラグイン一覧の取得に失敗しました。ネットワーク接続を確認してください。" に類似しているローカライズされた文字列を検索します。
+ /// "NuGet̃vOCꗗ̎擾Ɏs܂Blbg[NڑmFĂB" ɗގĂ郍[JCYꂽ܂B
///
public static string NuGetSearchFailed => ResourceManager.GetString("NuGetSearchFailed", resourceCulture) ?? string.Empty;
///
- /// "{0}のOCR機能が使えません。対象の言語機能をインストールしてください" に類似しているローカライズされた文字列を検索します。
+ /// "{0}OCR@\g܂BΏۂ̌@\CXg[Ă" ɗގĂ郍[JCYꂽ܂B
///
public static string OcrLanguageNotAvailable => ResourceManager.GetString("OcrLanguageNotAvailable", resourceCulture) ?? string.Empty;
///
- /// "認識モジュール" に類似しているローカライズされた文字列を検索します。
+ /// "FW[" ɗގĂ郍[JCYꂽ܂B
///
public static string OcrModule => ResourceManager.GetString("OcrModule", resourceCulture) ?? string.Empty;
///
- /// "OK" に類似しているローカライズされた文字列を検索します。
+ /// "OK" ɗގĂ郍[JCYꂽ܂B
///
public static string OK => ResourceManager.GetString("OK", resourceCulture) ?? string.Empty;
///
- /// "詳細情報の確認" に類似しているローカライズされた文字列を検索します。
+ /// "ڍ̊mF" ɗގĂ郍[JCYꂽ܂B
///
public static string OpenChangelogCommand => ResourceManager.GetString("OpenChangelogCommand", resourceCulture) ?? string.Empty;
///
- /// "サードパーティーライセンスの確認" に類似しているローカライズされた文字列を検索します。
+ /// "T[hp[eB[CZX̊mF" ɗގĂ郍[JCYꂽ܂B
///
public static string OpenThirdPartyLicensesCommand => ResourceManager.GetString("OpenThirdPartyLicensesCommand", resourceCulture) ?? string.Empty;
///
- /// "オーバレイ" に類似しているローカライズされた文字列を検索します。
+ /// "I[oC" ɗގĂ郍[JCYꂽ܂B
///
public static string Overlay => ResourceManager.GetString("Overlay", resourceCulture) ?? string.Empty;
///
- /// "オーバーレイ背景の不透明度" に類似しているローカライズされた文字列を検索します。
+ /// "I[o[Cwi̕sx" ɗގĂ郍[JCYꂽ܂B
///
public static string OverlayOpacity => ResourceManager.GetString("OverlayOpacity", resourceCulture) ?? string.Empty;
///
- /// "オーバーレイ切り替え" に類似しているローカライズされた文字列を検索します。
+ /// "I[o[Cւ" ɗގĂ郍[JCYꂽ܂B
///
public static string OverlayShortcut => ResourceManager.GetString("OverlayShortcut", resourceCulture) ?? string.Empty;
///
- /// "オーバーレイ表示の切り替え" に類似しているローカライズされた文字列を検索します。
+ /// "I[o[C\̐ւ" ɗގĂ郍[JCYꂽ܂B
///
public static string OverlaySwitch => ResourceManager.GetString("OverlaySwitch", resourceCulture) ?? string.Empty;
///
- /// "プラグイン設定" に類似しているローカライズされた文字列を検索します。
+ /// "vOCݒ" ɗގĂ郍[JCYꂽ܂B
///
public static string Plugin => ResourceManager.GetString("Plugin", resourceCulture) ?? string.Empty;
///
- /// "インストール失敗" に類似しているローカライズされた文字列を検索します。
+ /// "CXg[s" ɗގĂ郍[JCYꂽ܂B
///
public static string PluginInstallFailed => ResourceManager.GetString("PluginInstallFailed", resourceCulture) ?? string.Empty;
///
- /// "インストール完了" に類似しているローカライズされた文字列を検索します。
+ /// "CXg[" ɗގĂ郍[JCYꂽ܂B
///
public static string PluginInstallSuccess => ResourceManager.GetString("PluginInstallSuccess", resourceCulture) ?? string.Empty;
///
- /// "プラグインストア" に類似しているローカライズされた文字列を検索します。
+ /// "vOCXgA" ɗގĂ郍[JCYꂽ܂B
///
public static string PluginStore => ResourceManager.GetString("PluginStore", resourceCulture) ?? string.Empty;
///
- /// "プロジェクトページ" に類似しているローカライズされた文字列を検索します。
+ /// "vWFNgy[W" ɗގĂ郍[JCYꂽ܂B
///
public static string ProjectUrl => ResourceManager.GetString("ProjectUrl", resourceCulture) ?? string.Empty;
///
- /// "公開ページ" に類似しているローカライズされた文字列を検索します。
+ /// "Jy[W" ɗގĂ郍[JCYꂽ܂B
///
public static string PublishPage => ResourceManager.GetString("PublishPage", resourceCulture) ?? string.Empty;
///
- /// "{0}を自動起動に登録しました。" に類似しているローカライズされた文字列を検索します。
+ /// "{0}Nɓo^܂B" ɗގĂ郍[JCYꂽ܂B
///
public static string RegisterAutoStart => ResourceManager.GetString("RegisterAutoStart", resourceCulture) ?? string.Empty;
///
- /// "プラグインの変更を適用するには、WindowTranslatorを再起動してください。" に類似しているローカライズされた文字列を検索します。
+ /// "vOC̕ύXKpɂ́AWindowTranslatorċNĂB" ɗގĂ郍[JCYꂽ܂B
///
public static string RestartRequired => ResourceManager.GetString("RestartRequired", resourceCulture) ?? string.Empty;
///
- /// "後で" に類似しているローカライズされた文字列を検索します。
+ /// "" ɗގĂ郍[JCYꂽ܂B
///
public static string ReviewLater => ResourceManager.GetString("ReviewLater", resourceCulture) ?? string.Empty;
///
- /// "二度と表示しない" に類似しているローカライズされた文字列を検索します。
+ /// "xƕ\Ȃ" ɗގĂ郍[JCYꂽ܂B
///
public static string ReviewNeverShowAgain => ResourceManager.GetString("ReviewNeverShowAgain", resourceCulture) ?? string.Empty;
///
- /// "レビューのお願い" に類似しているローカライズされた文字列を検索します。
+ /// "r[̂肢" ɗގĂ郍[JCYꂽ܂B
///
public static string ReviewRequest => ResourceManager.GetString("ReviewRequest", resourceCulture) ?? string.Empty;
///
- /// "WindowTranslatorをご利用いただきありがとうございます。Microsoft Store..." に類似しているローカライズされた文字列を検索します。
+ /// "WindowTranslatorp肪Ƃ܂BMicrosoft Store..." ɗގĂ郍[JCYꂽ܂B
///
public static string ReviewRequestMessage => ResourceManager.GetString("ReviewRequestMessage", resourceCulture) ?? string.Empty;
///
- /// "そのまま実行" に類似しているローカライズされた文字列を検索します。
+ /// "̂܂s" ɗގĂ郍[JCYꂽ܂B
///
public static string RunAsIs => ResourceManager.GetString("RunAsIs", resourceCulture) ?? string.Empty;
///
- /// "翻訳元言語と翻訳先言語が同一です。異なる言語を指定してください。" に類似しているローカライズされた文字列を検索します。
+ /// "|Ɩ|挾ꂪłBقȂ錾w肵ĂB" ɗގĂ郍[JCYꂽ܂B
///
public static string SameSourceTargetLanguage => ResourceManager.GetString("SameSourceTargetLanguage", resourceCulture) ?? string.Empty;
///
- /// "保存して閉じる" に類似しているローカライズされた文字列を検索します。
+ /// "ۑĕ" ɗގĂ郍[JCYꂽ܂B
///
public static string SaveAndClose => ResourceManager.GetString("SaveAndClose", resourceCulture) ?? string.Empty;
///
- /// "WindowTranslator以外のウィンドウを選択してください" に類似しているローカライズされた文字列を検索します。
+ /// "WindowTranslatorȊÕEBhEIĂ" ɗގĂ郍[JCYꂽ܂B
///
public static string SelectOtherWindow => ResourceManager.GetString("SelectOtherWindow", resourceCulture) ?? string.Empty;
///
- /// "エラー情報をレポートシステムに送信します。以下の情報が送信されます。 * アプリ情報..." に類似しているローカライズされた文字列を検索します。
+ /// "G[|[gVXeɑM܂Bȉ̏M܂B ..." ɗގĂ郍[JCYꂽ܂B
///
public static string SendReportToolTip => ResourceManager.GetString("SendReportToolTip", resourceCulture) ?? string.Empty;
///
- /// "情報を送信" に類似しているローカライズされた文字列を検索します。
+ /// "𑗐M" ɗގĂ郍[JCYꂽ܂B
///
public static string SendRerpot => ResourceManager.GetString("SendRerpot", resourceCulture) ?? string.Empty;
///
- /// "送信完了" に類似しているローカライズされた文字列を検索します。
+ /// "M" ɗގĂ郍[JCYꂽ܂B
///
public static string Sent => ResourceManager.GetString("Sent", resourceCulture) ?? string.Empty;
///
- /// ":tired-face: **このまま保存しても動作しません。** ***&..." に類似しているローカライズされた文字列を検索します。
+ /// ":tired-face: **̂܂ܕۑĂ삵܂B** 
..." ɗގĂ郍[JCYꂽ܂B
///
public static string SettingInvalidContent => ResourceManager.GetString("SettingInvalidContent", resourceCulture) ?? string.Empty;
///
- /// "設定" に類似しているローカライズされた文字列を検索します。
+ /// "ݒ" ɗގĂ郍[JCYꂽ܂B
///
public static string Settings => ResourceManager.GetString("Settings", resourceCulture) ?? string.Empty;
///
- /// "設定検証エラー" に類似しているローカライズされた文字列を検索します。
+ /// "ݒ茟G[" ɗގĂ郍[JCYꂽ܂B
///
public static string SettingsInvalid => ResourceManager.GetString("SettingsInvalid", resourceCulture) ?? string.Empty;
///
- /// "全体設定" に類似しているローカライズされた文字列を検索します。
+ /// "S̐ݒ" ɗގĂ郍[JCYꂽ܂B
///
public static string SettingsViewModel => ResourceManager.GetString("SettingsViewModel", resourceCulture) ?? string.Empty;
///
- /// "ショートカット" に類似しているローカライズされた文字列を検索します。
+ /// "V[gJbg" ɗގĂ郍[JCYꂽ܂B
///
public static string Shortcut => ResourceManager.GetString("Shortcut", resourceCulture) ?? string.Empty;
///
- /// "翻訳元(認識)言語" に類似しているローカライズされた文字列を検索します。
+ /// "|(F)" ɗގĂ郍[JCYꂽ܂B
///
public static string Source => ResourceManager.GetString("Source", resourceCulture) ?? string.Empty;
///
- /// "Steamでゲームをギフト" に類似しているローカライズされた文字列を検索します。
+ /// "SteamŃQ[Mtg" ɗގĂ郍[JCYꂽ܂B
///
public static string SteamWishlist => ResourceManager.GetString("SteamWishlist", resourceCulture) ?? string.Empty;
///
- /// "送信" に類似しているローカライズされた文字列を検索します。
+ /// "M" ɗގĂ郍[JCYꂽ܂B
///
public static string Submit => ResourceManager.GetString("Submit", resourceCulture) ?? string.Empty;
///
- /// "翻訳先(表示)言語" に類似しているローカライズされた文字列を検索します。
+ /// "|(\)" ɗގĂ郍[JCYꂽ܂B
///
public static string Target => ResourceManager.GetString("Target", resourceCulture) ?? string.Empty;
///
- /// "翻訳対象プロセス" に類似しているローカライズされた文字列を検索します。
+ /// "|ΏۃvZX" ɗގĂ郍[JCYꂽ܂B
///
public static string TargetProcesses => ResourceManager.GetString("TargetProcesses", resourceCulture) ?? string.Empty;
///
- /// "対象ごとの設定" に類似しているローカライズされた文字列を検索します。
+ /// "ΏۂƂ̐ݒ" ɗގĂ郍[JCYꂽ܂B
///
public static string TargetSpecificSettings => ResourceManager.GetString("TargetSpecificSettings", resourceCulture) ?? string.Empty;
///
- /// "アプリ名" に類似しているローカライズされた文字列を検索します。
+ /// "Av" ɗގĂ郍[JCYꂽ܂B
///
public static string Title => ResourceManager.GetString("Title", resourceCulture) ?? string.Empty;
///
- /// "押してON/OFFを切り替える" に類似しているローカライズされた文字列を検索します。
+ /// "ON/OFFւ" ɗގĂ郍[JCYꂽ܂B
///
public static string Toggle => ResourceManager.GetString("Toggle", resourceCulture) ?? string.Empty;
///
- /// "言語設定" に類似しているローカライズされた文字列を検索します。
+ /// "ݒ" ɗގĂ郍[JCYꂽ܂B
///
public static string TranslateLanguage => ResourceManager.GetString("TranslateLanguage", resourceCulture) ?? string.Empty;
///
- /// "翻訳モジュール" に類似しているローカライズされた文字列を検索します。
+ /// "|W[" ɗގĂ郍[JCYꂽ܂B
///
public static string TranslateModule => ResourceManager.GetString("TranslateModule", resourceCulture) ?? string.Empty;
///
- /// "不明なエラーが発生しました" に類似しているローカライズされた文字列を検索します。
+ /// "sȃG[܂" ɗގĂ郍[JCYꂽ܂B
///
public static string UnhundledErrorMessage => ResourceManager.GetString("UnhundledErrorMessage", resourceCulture) ?? string.Empty;
///
- /// "アンインストール" に類似しているローカライズされた文字列を検索します。
+ /// "ACXg[" ɗގĂ郍[JCYꂽ܂B
///
public static string Uninstall => ResourceManager.GetString("Uninstall", resourceCulture) ?? string.Empty;
///
- /// "{0} をアンインストールしますか?次回起動時に完全に削除されます。" に類似しているローカライズされた文字列を検索します。
+ /// "{0} ACXg[܂H" ɗގĂ郍[JCYꂽ܂B
///
public static string UninstallConfirm => ResourceManager.GetString("UninstallConfirm", resourceCulture) ?? string.Empty;
///
- /// "選択したウィンドウ「{0}」はプロセスを特定できないため、キャプチャー出来ません。 ..." に類似しているローカライズされた文字列を検索します。
+ /// "IEBhEu{0}v̓vZXłȂ߁ALv`[o܂B ..." ɗގĂ郍[JCYꂽ܂B
///
public static string UnknownWindow => ResourceManager.GetString("UnknownWindow", resourceCulture) ?? string.Empty;
///
- /// "{0}の自動起動を解除しました。" に類似しているローカライズされた文字列を検索します。
+ /// "{0}̎N܂B" ɗގĂ郍[JCYꂽ܂B
///
public static string UnregisterAutoStart => ResourceManager.GetString("UnregisterAutoStart", resourceCulture) ?? string.Empty;
///
- /// "更新" に類似しているローカライズされた文字列を検索します。
+ /// "XV" ɗގĂ郍[JCYꂽ܂B
///
public static string Update => ResourceManager.GetString("Update", resourceCulture) ?? string.Empty;
///
- /// "更新あり" に類似しているローカライズされた文字列を検索します。
+ /// "XV" ɗގĂ郍[JCYꂽ܂B
///
public static string UpdateAvailable => ResourceManager.GetString("UpdateAvailable", resourceCulture) ?? string.Empty;
///
- /// "インストール済み: {0} → 最新: {1}" に類似しているローカライズされた文字列を検索します。
+ /// "CXg[ς: {0} ŐV: {1}" ɗގĂ郍[JCYꂽ܂B
///
public static string UpdateAvailableVersion => ResourceManager.GetString("UpdateAvailableVersion", resourceCulture) ?? string.Empty;
///
- /// "最新バージョンに更新" に類似しているローカライズされた文字列を検索します。
+ /// "ŐVo[WɍXV" ɗގĂ郍[JCYꂽ܂B
///
public static string UpdateCommand => ResourceManager.GetString("UpdateCommand", resourceCulture) ?? string.Empty;
///
- /// "更新情報" に類似しているローカライズされた文字列を検索します。
+ /// "XV" ɗގĂ郍[JCYꂽ܂B
///
public static string UpdateInfo => ResourceManager.GetString("UpdateInfo", resourceCulture) ?? string.Empty;
///
- /// "バージョン" に類似しているローカライズされた文字列を検索します。
+ /// "o[W" ɗގĂ郍[JCYꂽ܂B
///
public static string Version => ResourceManager.GetString("Version", resourceCulture) ?? string.Empty;
///
- /// "翻訳結果表示モード" に類似しているローカライズされた文字列を検索します。
+ /// "|ʕ\[h" ɗގĂ郍[JCYꂽ܂B
///
public static string ViewMode => ResourceManager.GetString("ViewMode", resourceCulture) ?? string.Empty;
///
- /// "Windows標準文字認識" に類似しているローカライズされた文字列を検索します。
+ /// "WindowsWF" ɗގĂ郍[JCYꂽ܂B
///
public static string WindowsMediaOcr => ResourceManager.GetString("WindowsMediaOcr", resourceCulture) ?? string.Empty;
///
- /// "レビューする" に類似しているローカライズされた文字列を検索します。
+ /// "r[" ɗގĂ郍[JCYꂽ܂B
///
public static string WriteReview => ResourceManager.GetString("WriteReview", resourceCulture) ?? string.Empty;
}
diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx
index b2737646..1d0e33af 100644
--- a/WindowTranslator/Properties/Resources.ar.resx
+++ b/WindowTranslator/Properties/Resources.ar.resx
@@ -466,7 +466,7 @@
إلغاء التثبيت
- هل أنت متأكد من رغبتك في إلغاء تثبيت {0}؟ سيتم حذفه بالكامل عند التشغيل التالي.
+ هل أنت متأكد من رغبتك في إلغاء تثبيت {0}؟
يتوفر تحديث
diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx
index a488938a..7438924c 100644
--- a/WindowTranslator/Properties/Resources.cs.resx
+++ b/WindowTranslator/Properties/Resources.cs.resx
@@ -356,7 +356,7 @@ Monitory nejsou podporovány.
Odinstalovat
- Opravdu chcete odinstalovat {0}? Bude zcela odstraněn při příštím spuštění.
+ Opravdu chcete odinstalovat {0}?
Dostupná aktualizace
diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx
index 9df7938a..dbdf0186 100644
--- a/WindowTranslator/Properties/Resources.de.resx
+++ b/WindowTranslator/Properties/Resources.de.resx
@@ -475,7 +475,7 @@ Monitore werden nicht unterstützt.
Deinstallieren
- Möchten Sie {0} wirklich deinstallieren? Es wird beim nächsten Start vollständig entfernt.
+ Möchten Sie {0} wirklich deinstallieren?
Update verfügbar
diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx
index db123fe1..6d0acccf 100644
--- a/WindowTranslator/Properties/Resources.en.resx
+++ b/WindowTranslator/Properties/Resources.en.resx
@@ -475,7 +475,7 @@ Monitors are not supported.
Uninstall
- Are you sure you want to uninstall {0}? It will be fully removed on next startup.
+ Are you sure you want to uninstall {0}?
Update available
diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx
index 3c443c0e..bf6977f4 100644
--- a/WindowTranslator/Properties/Resources.es.resx
+++ b/WindowTranslator/Properties/Resources.es.resx
@@ -466,7 +466,7 @@
Desinstalar
- ¿Está seguro de que desea desinstalar {0}? Se eliminará completamente en el próximo inicio.
+ ¿Está seguro de que desea desinstalar {0}?
Actualización disponible
diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx
index 1f9e5f5e..a5bacfea 100644
--- a/WindowTranslator/Properties/Resources.fa.resx
+++ b/WindowTranslator/Properties/Resources.fa.resx
@@ -460,7 +460,7 @@
حذف
- آیا مطمئن هستید که میخواهید {0} را حذف کنید؟ در راهاندازی بعدی به طور کامل حذف خواهد شد.
+ آیا مطمئن هستید که میخواهید {0} را حذف کنید؟
بهروزرسانی موجود است
diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx
index 97d5ae89..44c7b38d 100644
--- a/WindowTranslator/Properties/Resources.fil.resx
+++ b/WindowTranslator/Properties/Resources.fil.resx
@@ -475,7 +475,7 @@ Ang monitor ay hindi suportado.
I-uninstall
- Sigurado ka bang gusto mong i-uninstall ang {0}? Ito ay ganap na matatanggal sa susunod na pagsisimula.
+ Sigurado ka bang gusto mong i-uninstall ang {0}?
Available ang update
diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx
index 7192e2a7..2e988292 100644
--- a/WindowTranslator/Properties/Resources.fr.resx
+++ b/WindowTranslator/Properties/Resources.fr.resx
@@ -466,7 +466,7 @@
Désinstaller
- Voulez-vous vraiment désinstaller {0} ? Il sera complètement supprimé au prochain démarrage.
+ Voulez-vous vraiment désinstaller {0} ?
Mise à jour disponible
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index 90938454..0d475cbd 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -468,7 +468,7 @@
अनइंस्टॉल करें
- क्या आप {0} को अनइंस्टॉल करना चाहते हैं? यह अगली बार शुरू होने पर पूरी तरह हटा दिया जाएगा।
+ क्या आप {0} को अनइंस्टॉल करना चाहते हैं?
अपडेट उपलब्ध
@@ -503,4 +503,4 @@
लाइसेंस जानकारी
-
\ No newline at end of file
+
diff --git a/WindowTranslator/Properties/Resources.hu.resx b/WindowTranslator/Properties/Resources.hu.resx
index 3d443905..3d6eb384 100644
--- a/WindowTranslator/Properties/Resources.hu.resx
+++ b/WindowTranslator/Properties/Resources.hu.resx
@@ -356,7 +356,7 @@ A monitorok nem támogatottak.
Eltávolítás
- Biztosan eltávolítja a(z) {0} bővítményt? A következő indításkor teljesen törlődik.
+ Biztosan eltávolítja a(z) {0} bővítményt?
Frissítés érhető el
diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx
index 831b9951..1e5935b6 100644
--- a/WindowTranslator/Properties/Resources.id.resx
+++ b/WindowTranslator/Properties/Resources.id.resx
@@ -474,7 +474,7 @@ Monitor tidak didukung.
Hapus
- Apakah Anda yakin ingin menghapus {0}? Ini akan dihapus sepenuhnya saat startup berikutnya.
+ Apakah Anda yakin ingin menghapus {0}?
Pembaruan tersedia
diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx
index c8975d58..6843817f 100644
--- a/WindowTranslator/Properties/Resources.ko.resx
+++ b/WindowTranslator/Properties/Resources.ko.resx
@@ -475,7 +475,7 @@
제거
- {0}을(를) 제거하시겠습니까? 다음 시작 시 완전히 제거됩니다.
+ {0}을(를) 제거하시겠습니까?
업데이트 있음
diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx
index 4b22e3f2..35fcf87a 100644
--- a/WindowTranslator/Properties/Resources.ms.resx
+++ b/WindowTranslator/Properties/Resources.ms.resx
@@ -474,7 +474,7 @@ Monitor tidak disokong.
Nyahpasang
- Adakah anda pasti mahu menyahpasang {0}? Ia akan dibuang sepenuhnya semasa permulaan seterusnya.
+ Adakah anda pasti mahu menyahpasang {0}?
Kemaskini tersedia
diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx
index 163c956e..a7f2a347 100644
--- a/WindowTranslator/Properties/Resources.pl.resx
+++ b/WindowTranslator/Properties/Resources.pl.resx
@@ -475,7 +475,7 @@ Monitory nie są obsługiwane.
Odinstaluj
- Czy na pewno chcesz odinstalować {0}? Zostanie całkowicie usunięty przy następnym uruchomieniu.
+ Czy na pewno chcesz odinstalować {0}?
Dostępna aktualizacja
diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx
index 264d0454..57e5427a 100644
--- a/WindowTranslator/Properties/Resources.pt-BR.resx
+++ b/WindowTranslator/Properties/Resources.pt-BR.resx
@@ -474,7 +474,7 @@ Monitor tidak didukung.
Desinstalar
- Tem certeza que deseja desinstalar {0}? Ele será completamente removido na próxima inicialização.
+ Tem certeza que deseja desinstalar {0}?
Atualização disponível
diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx
index 5f447758..098a8db5 100644
--- a/WindowTranslator/Properties/Resources.resx
+++ b/WindowTranslator/Properties/Resources.resx
@@ -475,7 +475,7 @@
アンインストール
- {0} をアンインストールしますか?次回起動時に完全に削除されます。
+ {0} をアンインストールしますか?
更新あり
diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx
index 58f21d91..e6c2f57b 100644
--- a/WindowTranslator/Properties/Resources.ru.resx
+++ b/WindowTranslator/Properties/Resources.ru.resx
@@ -466,7 +466,7 @@
Удалить
- Вы уверены, что хотите удалить {0}? Он будет полностью удалён при следующем запуске.
+ Вы уверены, что хотите удалить {0}?
Доступно обновление
diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx
index 14c7279d..32b18f69 100644
--- a/WindowTranslator/Properties/Resources.th.resx
+++ b/WindowTranslator/Properties/Resources.th.resx
@@ -475,7 +475,7 @@
ถอนการติดตั้ง
- คุณแน่ใจหรือไม่ว่าต้องการถอนการติดตั้ง {0}? จะถูกลบออกอย่างสมบูรณ์เมื่อเริ่มต้นครั้งถัดไป
+ คุณแน่ใจหรือไม่ว่าต้องการถอนการติดตั้ง {0}?
มีการอัปเดต
diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx
index da9222d8..14d89982 100644
--- a/WindowTranslator/Properties/Resources.tr.resx
+++ b/WindowTranslator/Properties/Resources.tr.resx
@@ -475,7 +475,7 @@ Monitör desteklenmiyor.
Kaldır
- {0} öğesini kaldırmak istediğinizden emin misiniz? Sonraki başlatmada tamamen silinecek.
+ {0} öğesini kaldırmak istediğinizden emin misiniz?
Güncelleme mevcut
diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx
index ff57858d..1e8d917e 100644
--- a/WindowTranslator/Properties/Resources.vi.resx
+++ b/WindowTranslator/Properties/Resources.vi.resx
@@ -475,7 +475,7 @@ Màn hình không được hỗ trợ.
Gỡ cài đặt
- Bạn có chắc muốn gỡ cài đặt {0}? Nó sẽ được xóa hoàn toàn khi khởi động lại.
+ Bạn có chắc muốn gỡ cài đặt {0}?
Có cập nhật
diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx
index 2958ffa4..9302d7c2 100644
--- a/WindowTranslator/Properties/Resources.zh-CN.resx
+++ b/WindowTranslator/Properties/Resources.zh-CN.resx
@@ -475,7 +475,7 @@
卸载
- 确定要卸载 {0} 吗?下次启动时将完全移除。
+ 确定要卸载 {0} 吗?
有更新
diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx
index 0e3cf99a..f6e61784 100644
--- a/WindowTranslator/Properties/Resources.zh-TW.resx
+++ b/WindowTranslator/Properties/Resources.zh-TW.resx
@@ -475,7 +475,7 @@
解除安裝
- 確定要解除安裝 {0} 嗎?下次啟動時將完全移除。
+ 確定要解除安裝 {0} 嗎?
有更新
diff --git a/docs/plugin.md b/docs/plugin.md
index bcbad419..a5644d0a 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -26,9 +26,10 @@ cd WindowTranslator.Plugin.YourPlugin
WindowTranslator.Plugin.YourPlugin
+ WindowTranslator Your Plugin
1.0.0
YourName
- 説明文
+ プラグインストアに表示する具体的な説明文
$(PackageTags);windowtranslator-plugin
MIT
@@ -47,6 +48,14 @@ cd WindowTranslator.Plugin.YourPlugin
> **重要**: `` に `windowtranslator-plugin` を含めることで、
> WindowTranslator アプリ内のプラグインストアに表示されます。
+>
+> ``、``、``、プロジェクトURL、ライセンス情報は
+> プラグインストアの一覧・詳細に表示されます。利用者が機能と提供元を判断できる
+> 内容を設定してください。
+>
+> `WindowTranslator.Abstractions` の依存バージョン範囲は、インストール先の
+> WindowTranslator との互換性判定に使用されます。サポートする最も古い
+> `WindowTranslator.Abstractions` のバージョンを指定してください。
### 3. プラグインを実装
@@ -123,10 +132,18 @@ NuGetパッケージで宣言されたランタイム依存関係も再帰的に
同じ依存パッケージに両立しないバージョン条件がある場合は、既存の
プラグイン配置を変更せずにインストールを中止します。
+保存済みのモジュール選択やプラグイン設定パラメータだけを根拠に、パッケージが
+自動インストールされることはありません。インストールはプラグインストアで
+利用者が明示的に実行した場合だけ行われます。
+
+アンインストールすると管理フォルダのパッケージは直ちに削除されます。
+実行中に読み込まれたプラグインを停止するには、WindowTranslator の再起動が必要です。
+
## 注意事項
- プラグインは .NET 10 以上をターゲットにしてください
- `true` を必ず設定してください
- ホスト側で既に提供されているパッケージは `ExcludeAssets="runtime"` を設定し、DLL を重複させないようにしてください
- 通常のランタイム依存は `PackageReference` として宣言してください
+- `ProjectReference` は通常、参照先プロジェクトへの NuGet 依存としてパッケージ化されます。参照先をNuGetへ公開しない場合は、`PrivateAssets="all"` を設定したうえで必要なDLLをプラグインパッケージへ同梱し、参照先が必要とする `PackageReference` もプラグイン側で宣言してください
- パッケージ固有の追加ファイルは、実行時に必要な相対ディレクトリを保って `lib/net10.0/` に含めてください
From a5de0609e1ed55d5097a74a935e0b091406725a5 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sat, 1 Aug 2026 12:46:49 +0900
Subject: [PATCH 08/43] =?UTF-8?q?NuGet=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=81=AE=E3=83=95=E3=83=AC=E3=83=BC=E3=83=A0=E3=83=AF?=
=?UTF-8?q?=E3=83=BC=E3=82=AF=E5=88=A4=E5=AE=9A=E3=81=A8=E4=BE=9D=E5=AD=98?=
=?UTF-8?q?=E3=82=A2=E3=82=BB=E3=83=B3=E3=83=96=E3=83=AA=E8=A7=A3=E6=B1=BA?=
=?UTF-8?q?=E3=82=92=E6=94=B9=E5=96=84?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Directory.Packages.props | 1 +
.../NuGetPluginServiceTests.cs | 70 ++++++++--
.../PluginStore/NuGetPackageInstaller.cs | 90 ++++++------
.../Modules/PluginStore/NuGetPluginCatalog.cs | 131 +++++++++++++++++-
WindowTranslator/WindowTranslator.csproj | 1 +
5 files changed, 233 insertions(+), 60 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 04ff4b36..bf4b2b97 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -34,6 +34,7 @@
+
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index d1eade8e..a6891928 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -1,12 +1,15 @@
using System.IO.Compression;
using System.Net;
using System.Net.Http;
+using System.Runtime.InteropServices;
+using System.Runtime.Loader;
using System.Text;
using System.Text.Json;
using System.Xml.Linq;
using Microsoft.Extensions.Logging.Abstractions;
using NuGet.Versioning;
using Weikio.PluginFramework.Catalogs;
+using Weikio.PluginFramework.Context;
using WindowTranslator.Modules;
using WindowTranslator.Modules.PluginStore;
@@ -14,6 +17,8 @@ namespace WindowTranslator.Tests;
public sealed class NuGetPluginServiceTests
{
+ private static readonly string RuntimeIdentifier = RuntimeInformation.RuntimeIdentifier;
+
[Fact]
public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories()
{
@@ -35,8 +40,8 @@ public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories
{
["lib/net10.0/Root.Plugin.dll"] = "root"u8.ToArray(),
["lib/net10.0/fr/Root.Plugin.resources.dll"] = "fr"u8.ToArray(),
- ["runtimes/win-x64/native/root-native.dll"] = "native"u8.ToArray(),
- ["lib/net10.0/runtimes/win-x64/native/custom-native.dll"] = "custom"u8.ToArray(),
+ [$"runtimes/{RuntimeIdentifier}/native/root-native.dll"] = "native"u8.ToArray(),
+ [$"lib/net10.0/runtimes/{RuntimeIdentifier}/native/custom-native.dll"] = "custom"u8.ToArray(),
}));
handler.AddPackage(
"Dependency.Package",
@@ -82,7 +87,7 @@ public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories
await File.ReadAllTextAsync(Path.Combine(
pluginDirectory,
"runtimes",
- "win-x64",
+ RuntimeIdentifier,
"native",
"root-native.dll")));
Assert.Equal(
@@ -90,7 +95,7 @@ await File.ReadAllTextAsync(Path.Combine(
await File.ReadAllTextAsync(Path.Combine(
pluginDirectory,
"runtimes",
- "win-x64",
+ RuntimeIdentifier,
"native",
"custom-native.dll")));
Assert.DoesNotContain(
@@ -296,7 +301,7 @@ public async Task DependencyWithIncompatibleLibStillInstallsCompatibleNativeAsse
new Dictionary
{
["lib/net48/LegacyOnly.dll"] = "legacy"u8.ToArray(),
- ["runtimes/win-x64/native/compatible.dll"] = "native"u8.ToArray(),
+ [$"runtimes/{RuntimeIdentifier}/native/compatible.dll"] = "native"u8.ToArray(),
}));
using var client = new HttpClient(handler);
@@ -312,7 +317,7 @@ await File.ReadAllTextAsync(Path.Combine(
testDirectory,
"Root.Plugin",
"runtimes",
- "win-x64",
+ RuntimeIdentifier,
"native",
"compatible.dll")));
}
@@ -635,6 +640,20 @@ public async Task CatalogLoadsARealAssemblyFromAPackageSubdirectory()
File.Copy(
testAssemblyPath,
Path.Combine(packageDirectory, Path.GetFileName(testAssemblyPath)));
+ var runtimeDirectory = Path.Combine(
+ packageDirectory,
+ "runtimes",
+ "win",
+ "lib",
+ "net10.0");
+ Directory.CreateDirectory(runtimeDirectory);
+ File.Copy(
+ typeof(NuGetVersion).Assembly.Location,
+ Path.Combine(runtimeDirectory, "NuGet.Versioning.dll"));
+ Assert.Empty(Directory.EnumerateFiles(
+ packageDirectory,
+ "*.deps.json",
+ SearchOption.AllDirectories));
var options = new FolderPluginCatalogOptions();
options.TypeFinderOptions.TypeFinderCriterias.Clear();
@@ -643,8 +662,19 @@ public async Task CatalogLoadsARealAssemblyFromAPackageSubdirectory()
Query = static (_, type) =>
type.Name == nameof(CatalogProbeTranslateModule),
});
- options.PluginLoadContextOptions.AdditionalRuntimePaths =
- [AppContext.BaseDirectory];
+ options.PluginLoadContextOptions.UseHostApplicationAssemblies =
+ UseHostApplicationAssembliesEnum.Selected;
+ options.PluginLoadContextOptions.HostApplicationAssemblies =
+ AssemblyLoadContext.Default.Assemblies
+ .Where(assembly => !assembly.IsDynamic
+ && assembly != typeof(NuGetPluginServiceTests).Assembly
+ && !string.Equals(
+ assembly.GetName().Name,
+ typeof(NuGetVersion).Assembly.GetName().Name,
+ StringComparison.OrdinalIgnoreCase))
+ .Select(assembly => assembly.GetName())
+ .ToList();
+ options.PluginLoadContextOptions.AdditionalRuntimePaths = [];
var catalog = new NuGetPluginCatalog(
sourceDirectory,
tempDirectory,
@@ -653,9 +683,13 @@ public async Task CatalogLoadsARealAssemblyFromAPackageSubdirectory()
await catalog.Initialize();
Assert.True(catalog.IsInitialized);
- Assert.Contains(
+ var plugin = Assert.Single(
catalog.GetPlugins(),
plugin => plugin.Type.Name == nameof(CatalogProbeTranslateModule));
+ var module = Assert.IsAssignableFrom(
+ Activator.CreateInstance(plugin.Type));
+ var translated = await module.TranslateAsync([new("source", null)]);
+ Assert.Equal("1.2.3", Assert.Single(translated));
}
finally
{
@@ -671,6 +705,20 @@ public void FrameworkSelectionPrefersTheCompatibleWindowsTarget()
"net10.0-windows10.0.20348.0",
NuGetPackageInstaller.SelectBestTfm(
["net10.0", "net10.0-windows10.0.20348.0", "netstandard2.0"]));
+ Assert.Equal(
+ "net10.0-windows10.0.19041.0",
+ NuGetPackageInstaller.SelectBestTfm(
+ [
+ "net10.0",
+ "net10.0-windows10.0.19041.0",
+ "net10.0-windows10.0.22621.0",
+ ]));
+ Assert.Equal(
+ ".NETCoreApp,Version=v10.0",
+ NuGetPackageInstaller.SelectBestTfm([".NETCoreApp,Version=v10.0"]));
+ Assert.Null(NuGetPackageInstaller.SelectBestTfm(
+ ["net10.0-windows10.0.22621.0"]));
+ Assert.Null(NuGetPackageInstaller.SelectBestTfm(["net11.0-windows"]));
Assert.Null(NuGetPackageInstaller.SelectBestTfm(["net48"]));
}
@@ -822,5 +870,7 @@ public sealed class CatalogProbeTranslateModule : ITranslateModule
{
public ValueTask TranslateAsync(TextInfo[] srcTexts)
=> ValueTask.FromResult(
- Enumerable.Repeat(string.Empty, srcTexts.Length).ToArray());
+ Enumerable.Repeat(
+ new NuGetVersion(1, 2, 3).ToNormalizedString(),
+ srcTexts.Length).ToArray());
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
index 208ff456..726f6315 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
@@ -1,10 +1,14 @@
using System.IO;
using System.IO.Compression;
using System.Net.Http;
+using System.Reflection;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
+using NuGet.Frameworks;
using NuGet.Versioning;
namespace WindowTranslator.Modules.PluginStore;
@@ -19,26 +23,11 @@ internal sealed class NuGetPackageInstaller(
{
private const string FlatContainerBase = "https://api.nuget.org/v3-flatcontainer";
- private static readonly string[] CompatibleFrameworks =
- [
- "net10.0-windows",
- "net10.0",
- "net9.0-windows",
- "net9.0",
- "net8.0-windows",
- "net8.0",
- "net7.0-windows",
- "net7.0",
- "net6.0-windows",
- "net6.0",
- "net5.0-windows",
- "net5.0",
- "netcoreapp3.1",
- "netstandard2.1",
- "netstandard2.0",
- ];
-
- private static readonly string[] CompatibleRuntimeIdentifiers = ["win-x64", "win", "any"];
+ private static readonly NuGetFramework HostFramework = GetHostFramework();
+ private static readonly FrameworkReducer FrameworkReducer = new();
+
+ private static readonly string[] CompatibleRuntimeIdentifiers =
+ [RuntimeInformation.RuntimeIdentifier, "win", "any"];
private readonly HttpClient httpClient = httpClient;
private readonly ILogger logger = logger;
@@ -89,23 +78,44 @@ public async Task InstallAsync(
{
var candidates = frameworks
.Distinct(StringComparer.OrdinalIgnoreCase)
- .Select(original => (Original: original, Normalized: NormalizeFramework(original)))
+ .Select(original => (Original: original, Framework: ParseFramework(original)))
+ .Where(candidate => candidate.Framework is not null)
.ToArray();
- foreach (var compatibleFramework in CompatibleFrameworks)
+ var nearest = FrameworkReducer.GetNearest(
+ HostFramework,
+ candidates.Select(candidate => candidate.Framework!));
+ if (nearest is null)
{
- var match = candidates
- .OrderByDescending(c => c.Normalized, StringComparer.OrdinalIgnoreCase)
- .FirstOrDefault(c => compatibleFramework.EndsWith("-windows", StringComparison.Ordinal)
- ? c.Normalized.StartsWith(compatibleFramework, StringComparison.OrdinalIgnoreCase)
- : c.Normalized.Equals(compatibleFramework, StringComparison.OrdinalIgnoreCase));
- if (match.Original is not null)
- {
- return match.Original;
- }
+ return null;
}
- return null;
+ return candidates.First(candidate => NuGetFrameworkFullComparer.Instance.Equals(
+ candidate.Framework,
+ nearest)).Original;
+ }
+
+ private static NuGetFramework GetHostFramework()
+ {
+ var assembly = typeof(NuGetPackageInstaller).Assembly;
+ var frameworkName = assembly.GetCustomAttribute()?.FrameworkName
+ ?? throw new InvalidOperationException("WindowTranslator のターゲットフレームワークを取得できませんでした。");
+ var framework = NuGetFramework.Parse(frameworkName);
+ var platformName = assembly.GetCustomAttribute()?.PlatformName;
+ return string.IsNullOrWhiteSpace(platformName)
+ ? framework
+ : NuGetFramework.ParseFolder($"{framework.GetShortFolderName()}-{platformName}");
+ }
+
+ private static NuGetFramework? ParseFramework(string framework)
+ {
+ if (string.IsNullOrWhiteSpace(framework))
+ {
+ return null;
+ }
+
+ var parsed = NuGetFramework.Parse(framework);
+ return parsed.IsUnsupported ? null : parsed;
}
private async Task> ResolvePackageGraphAsync(
@@ -597,22 +607,6 @@ private static bool StreamsEqual(Stream left, Stream right)
return right.ReadByte() == -1;
}
- private static string NormalizeFramework(string framework)
- {
- var normalized = framework.Replace(" ", string.Empty, StringComparison.Ordinal);
- const string netCoreAppPrefix = ".NETCoreApp,Version=v";
- const string netStandardPrefix = ".NETStandard,Version=v";
- if (normalized.StartsWith(netCoreAppPrefix, StringComparison.OrdinalIgnoreCase))
- {
- return $"net{normalized[netCoreAppPrefix.Length..]}";
- }
- if (normalized.StartsWith(netStandardPrefix, StringComparison.OrdinalIgnoreCase))
- {
- return $"netstandard{normalized[netStandardPrefix.Length..]}";
- }
- return normalized;
- }
-
private static void ValidatePackageId(string packageId)
{
if (string.IsNullOrWhiteSpace(packageId)
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index b1d82876..e25a450b 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -1,6 +1,8 @@
using System.IO;
+using System.Reflection.PortableExecutable;
using Weikio.PluginFramework.Abstractions;
using Weikio.PluginFramework.Catalogs;
+using Weikio.PluginFramework.Context;
namespace WindowTranslator.Modules.PluginStore;
@@ -15,7 +17,8 @@ public sealed class NuGetPluginCatalog : IPluginCatalog
private readonly string sourceDir;
private readonly string tempDir;
- private readonly FolderPluginCatalog innerCatalog;
+ private readonly FolderPluginCatalogOptions options;
+ private CompositePluginCatalog innerCatalog = new();
public NuGetPluginCatalog(string sourceDir, FolderPluginCatalogOptions options)
: this(sourceDir, DefaultTempDir, options)
@@ -26,7 +29,7 @@ internal NuGetPluginCatalog(string sourceDir, string tempDir, FolderPluginCatalo
{
this.sourceDir = sourceDir;
this.tempDir = tempDir;
- this.innerCatalog = new FolderPluginCatalog(tempDir, options);
+ this.options = options;
}
///
@@ -37,6 +40,7 @@ public async Task Initialize()
{
SynchronizePluginFiles(this.sourceDir, this.tempDir);
+ this.innerCatalog = CreateCatalog(this.tempDir, this.options);
await this.innerCatalog.Initialize().ConfigureAwait(false);
}
@@ -46,6 +50,127 @@ public async Task Initialize()
///
public Plugin Get(string name, Version version) => this.innerCatalog.Get(name, version);
+ private static CompositePluginCatalog CreateCatalog(
+ string directory,
+ FolderPluginCatalogOptions baseOptions)
+ {
+ var catalogs = new List
+ {
+ new FolderPluginCatalog(
+ directory,
+ CreateCatalogOptions(
+ baseOptions,
+ directory,
+ SearchOption.TopDirectoryOnly,
+ includeSubfolders: false)),
+ };
+
+ foreach (var packageDirectory in Directory
+ .EnumerateDirectories(directory)
+ .OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
+ {
+ catalogs.Add(new FolderPluginCatalog(
+ packageDirectory,
+ CreateCatalogOptions(
+ baseOptions,
+ packageDirectory,
+ SearchOption.AllDirectories,
+ includeSubfolders: true)));
+ }
+
+ return new CompositePluginCatalog([.. catalogs]);
+ }
+
+ private static FolderPluginCatalogOptions CreateCatalogOptions(
+ FolderPluginCatalogOptions baseOptions,
+ string pluginDirectory,
+ SearchOption searchOption,
+ bool includeSubfolders)
+ {
+ var baseLoadOptions = baseOptions.PluginLoadContextOptions;
+ var files = Directory
+ .EnumerateFiles(pluginDirectory, "*", searchOption)
+ .Where(path => path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
+ || path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
+ .Select(path => new PluginFile(
+ path,
+ Path.GetRelativePath(pluginDirectory, path),
+ IsManagedAssembly(path)))
+ .OrderBy(file => GetRuntimeAssetPriority(file.RelativePath))
+ .ThenBy(file => file.RelativePath, StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
+ var runtimeHints = new List(
+ baseLoadOptions.RuntimeAssemblyHints ?? []);
+ var hintKeys = runtimeHints
+ .Select(hint => GetHintKey(hint.FileName, hint.IsNative))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var file in files.OrderByDescending(file => file.IsManaged))
+ {
+ var isNative = !file.IsManaged;
+ if (hintKeys.Add(GetHintKey(Path.GetFileName(file.Path), isNative)))
+ {
+ runtimeHints.Add(new RuntimeAssemblyHint(
+ Path.GetFileName(file.Path),
+ file.Path,
+ isNative));
+ }
+ }
+
+ var additionalRuntimePaths = new List(
+ baseLoadOptions.AdditionalRuntimePaths ?? []);
+ foreach (var path in files
+ .Where(file => file.IsManaged)
+ .Select(file => Path.GetDirectoryName(file.Path)!)
+ .Distinct(StringComparer.OrdinalIgnoreCase))
+ {
+ if (!additionalRuntimePaths.Contains(path, StringComparer.OrdinalIgnoreCase))
+ {
+ additionalRuntimePaths.Add(path);
+ }
+ }
+
+ return new FolderPluginCatalogOptions
+ {
+ IncludeSubfolders = includeSubfolders,
+ SearchPatterns = [.. baseOptions.SearchPatterns],
+ TypeFinderOptions = baseOptions.TypeFinderOptions,
+ PluginNameOptions = baseOptions.PluginNameOptions,
+ PluginLoadContextOptions = new PluginLoadContextOptions
+ {
+ UseHostApplicationAssemblies = baseLoadOptions.UseHostApplicationAssemblies,
+ HostApplicationAssemblies = [.. baseLoadOptions.HostApplicationAssemblies],
+ LoggerFactory = baseLoadOptions.LoggerFactory,
+ AdditionalRuntimePaths = additionalRuntimePaths,
+ RuntimeAssemblyHints = runtimeHints,
+ },
+ };
+ }
+
+ private static bool IsManagedAssembly(string path)
+ {
+ try
+ {
+ using var stream = File.OpenRead(path);
+ using var reader = new PEReader(stream);
+ return reader.HasMetadata;
+ }
+ catch (BadImageFormatException)
+ {
+ return false;
+ }
+ }
+
+ private static int GetRuntimeAssetPriority(string relativePath)
+ => relativePath.StartsWith(
+ $"runtimes{Path.DirectorySeparatorChar}",
+ StringComparison.OrdinalIgnoreCase)
+ ? 0
+ : 1;
+
+ private static string GetHintKey(string fileName, bool isNative)
+ => $"{(isNative ? 'N' : 'M')}:{fileName}";
+
internal static void SynchronizePluginFiles(string source, string destination)
{
Directory.CreateDirectory(destination);
@@ -196,4 +321,6 @@ private static int GetPathDepth(string path)
=> path.Count(character =>
character == Path.DirectorySeparatorChar
|| character == Path.AltDirectorySeparatorChar);
+
+ private sealed record PluginFile(string Path, string RelativePath, bool IsManaged);
}
diff --git a/WindowTranslator/WindowTranslator.csproj b/WindowTranslator/WindowTranslator.csproj
index e07e91be..f97b6cea 100644
--- a/WindowTranslator/WindowTranslator.csproj
+++ b/WindowTranslator/WindowTranslator.csproj
@@ -51,6 +51,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
From 9041b6b834bf772561b8fa4dca128c9cb62b0ce3 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sun, 2 Aug 2026 02:34:19 +0900
Subject: [PATCH 09/43] =?UTF-8?q?NuGet=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=81=AE=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF=E3=81=A8?=
=?UTF-8?q?=E7=AE=A1=E7=90=86=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../CatalogProbeResources.ar.resx | 18 +++
.../CatalogProbeResources.fr.resx | 18 +++
.../CatalogProbeResources.resx | 18 +++
.../NuGetPluginServiceTests.cs | 126 ++++++++++++++++
.../Modules/PluginStore/NuGetPluginCatalog.cs | 138 +++++++++++++++++-
.../PluginStore/PluginStoreViewModel.cs | 10 ++
docs/plugin.md | 2 +-
7 files changed, 321 insertions(+), 9 deletions(-)
create mode 100644 WindowTranslator.Tests/CatalogProbeResources.ar.resx
create mode 100644 WindowTranslator.Tests/CatalogProbeResources.fr.resx
create mode 100644 WindowTranslator.Tests/CatalogProbeResources.resx
diff --git a/WindowTranslator.Tests/CatalogProbeResources.ar.resx b/WindowTranslator.Tests/CatalogProbeResources.ar.resx
new file mode 100644
index 00000000..cec4452a
--- /dev/null
+++ b/WindowTranslator.Tests/CatalogProbeResources.ar.resx
@@ -0,0 +1,18 @@
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ العربية
+
+
diff --git a/WindowTranslator.Tests/CatalogProbeResources.fr.resx b/WindowTranslator.Tests/CatalogProbeResources.fr.resx
new file mode 100644
index 00000000..d96e511b
--- /dev/null
+++ b/WindowTranslator.Tests/CatalogProbeResources.fr.resx
@@ -0,0 +1,18 @@
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ français
+
+
diff --git a/WindowTranslator.Tests/CatalogProbeResources.resx b/WindowTranslator.Tests/CatalogProbeResources.resx
new file mode 100644
index 00000000..6bf0819a
--- /dev/null
+++ b/WindowTranslator.Tests/CatalogProbeResources.resx
@@ -0,0 +1,18 @@
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 日本語
+
+
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index a6891928..2eabbf91 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -1,6 +1,8 @@
+using System.Globalization;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
+using System.Resources;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
@@ -407,6 +409,42 @@ await File.WriteAllTextAsync(
}
}
+ [Fact]
+ public async Task PluginStoreKeepsInstalledPackagesVisibleWhenNuGetSearchFails()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(testDirectory, "nuget-manifest.json"),
+ JsonSerializer.Serialize(new InstalledManifest(
+ [new InstalledPackageInfo("Installed.Plugin", "1.2.3")])),
+ Encoding.UTF8);
+ using var handler = new InMemoryNuGetHandler();
+ using var client = new HttpClient(handler);
+ using var service = CreateService(client, testDirectory);
+ var viewModel = new PluginStoreViewModel(
+ service,
+ NullLogger.Instance,
+ dialogService: null!);
+
+ await viewModel.LoadAsync();
+
+ var package = Assert.Single(viewModel.Packages);
+ Assert.Equal("Installed.Plugin", package.Id);
+ Assert.Equal("1.2.3", package.InstalledVersion);
+ Assert.True(package.IsInstalled);
+ Assert.NotNull(viewModel.ErrorMessage);
+ Assert.Contains(
+ handler.RequestedPaths,
+ path => path.Equals("/v3/index.json", StringComparison.OrdinalIgnoreCase));
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
[Fact]
public async Task InstallRejectsPackageRequiringNewerHostAbstractions()
{
@@ -698,6 +736,81 @@ public async Task CatalogLoadsARealAssemblyFromAPackageSubdirectory()
}
}
+ [Fact]
+ public async Task CatalogLoadsTheSatelliteAssemblyForTheRequestedCulture()
+ {
+ var sourceDirectory = CreateTestDirectory();
+ var tempDirectory = CreateTestDirectory();
+ var originalCulture = CultureInfo.CurrentUICulture;
+ try
+ {
+ CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("fr-FR");
+ var packageDirectory = Path.Combine(sourceDirectory, "Catalog.Probe");
+ Directory.CreateDirectory(packageDirectory);
+ var testAssemblyPath = typeof(NuGetPluginServiceTests).Assembly.Location;
+ File.Copy(
+ testAssemblyPath,
+ Path.Combine(packageDirectory, Path.GetFileName(testAssemblyPath)));
+ foreach (var cultureName in new[] { "ar", "fr" })
+ {
+ var cultureDirectory = Path.Combine(packageDirectory, cultureName);
+ Directory.CreateDirectory(cultureDirectory);
+ File.Copy(
+ Path.Combine(
+ Path.GetDirectoryName(testAssemblyPath)!,
+ cultureName,
+ "WindowTranslator.Tests.resources.dll"),
+ Path.Combine(cultureDirectory, "WindowTranslator.Tests.resources.dll"));
+ }
+
+ var options = new FolderPluginCatalogOptions();
+ options.TypeFinderOptions.TypeFinderCriterias.Clear();
+ options.TypeFinderOptions.TypeFinderCriterias.Add(new()
+ {
+ Query = static (_, type) =>
+ type.Name == nameof(CatalogProbeLocalizedTranslateModule),
+ });
+ options.PluginNameOptions.PluginNameGenerator = static (_, type) =>
+ new ResourceManager(
+ "WindowTranslator.Tests.CatalogProbeResources",
+ type.Assembly).GetString("Greeting", CultureInfo.CurrentUICulture)
+ ?? type.Name;
+ options.PluginLoadContextOptions.UseHostApplicationAssemblies =
+ UseHostApplicationAssembliesEnum.Selected;
+ options.PluginLoadContextOptions.HostApplicationAssemblies =
+ AssemblyLoadContext.Default.Assemblies
+ .Where(assembly => !assembly.IsDynamic
+ && assembly != typeof(NuGetPluginServiceTests).Assembly)
+ .Select(assembly => assembly.GetName())
+ .ToList();
+ options.PluginLoadContextOptions.AdditionalRuntimePaths = [];
+ var catalog = new NuGetPluginCatalog(
+ sourceDirectory,
+ tempDirectory,
+ options);
+
+ await catalog.Initialize();
+
+ var plugin = Assert.Single(
+ catalog.GetPlugins(),
+ plugin => plugin.Type.Name == nameof(CatalogProbeLocalizedTranslateModule));
+ Assert.Equal("français", plugin.Name);
+ Assert.NotSame(
+ AssemblyLoadContext.Default,
+ AssemblyLoadContext.GetLoadContext(plugin.Type.Assembly));
+ var module = Assert.IsAssignableFrom(
+ Activator.CreateInstance(plugin.Type));
+ var translated = await module.TranslateAsync([new("source", null)]);
+ Assert.Equal("français", Assert.Single(translated));
+ }
+ finally
+ {
+ CultureInfo.CurrentUICulture = originalCulture;
+ DeleteTestDirectory(sourceDirectory);
+ DeleteTestDirectory(tempDirectory);
+ }
+ }
+
[Fact]
public void FrameworkSelectionPrefersTheCompatibleWindowsTarget()
{
@@ -874,3 +987,16 @@ public ValueTask TranslateAsync(TextInfo[] srcTexts)
new NuGetVersion(1, 2, 3).ToNormalizedString(),
srcTexts.Length).ToArray());
}
+
+public sealed class CatalogProbeLocalizedTranslateModule : ITranslateModule
+{
+ private static readonly ResourceManager Resources = new(
+ "WindowTranslator.Tests.CatalogProbeResources",
+ typeof(CatalogProbeLocalizedTranslateModule).Assembly);
+
+ public ValueTask TranslateAsync(TextInfo[] srcTexts)
+ => ValueTask.FromResult(
+ Enumerable.Repeat(
+ Resources.GetString("Greeting", CultureInfo.CurrentUICulture) ?? string.Empty,
+ srcTexts.Length).ToArray());
+}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index e25a450b..7a7e7dff 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -1,5 +1,7 @@
using System.IO;
+using System.Reflection;
using System.Reflection.PortableExecutable;
+using System.Runtime.Loader;
using Weikio.PluginFramework.Abstractions;
using Weikio.PluginFramework.Catalogs;
using Weikio.PluginFramework.Context;
@@ -92,20 +94,29 @@ private static FolderPluginCatalogOptions CreateCatalogOptions(
.EnumerateFiles(pluginDirectory, "*", searchOption)
.Where(path => path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
|| path.EndsWith(".exe", StringComparison.OrdinalIgnoreCase))
- .Select(path => new PluginFile(
- path,
- Path.GetRelativePath(pluginDirectory, path),
- IsManagedAssembly(path)))
+ .Select(path => CreatePluginFile(pluginDirectory, path))
.OrderBy(file => GetRuntimeAssetPriority(file.RelativePath))
.ThenBy(file => file.RelativePath, StringComparer.OrdinalIgnoreCase)
.ToArray();
+ var satelliteAssemblies = files
+ .Where(file => file.IsSatelliteAssembly)
+ .GroupBy(file => GetSatelliteKey(
+ file.AssemblyName!.Name!,
+ file.AssemblyName.CultureName!))
+ .ToDictionary(
+ group => group.Key,
+ group => group.First(),
+ StringComparer.OrdinalIgnoreCase);
+
var runtimeHints = new List(
baseLoadOptions.RuntimeAssemblyHints ?? []);
var hintKeys = runtimeHints
.Select(hint => GetHintKey(hint.FileName, hint.IsNative))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
- foreach (var file in files.OrderByDescending(file => file.IsManaged))
+ foreach (var file in files
+ .Where(file => !file.IsSatelliteAssembly)
+ .OrderByDescending(file => file.IsManaged))
{
var isNative = !file.IsManaged;
if (hintKeys.Add(GetHintKey(Path.GetFileName(file.Path), isNative)))
@@ -120,7 +131,7 @@ private static FolderPluginCatalogOptions CreateCatalogOptions(
var additionalRuntimePaths = new List(
baseLoadOptions.AdditionalRuntimePaths ?? []);
foreach (var path in files
- .Where(file => file.IsManaged)
+ .Where(file => file.IsManaged && !file.IsSatelliteAssembly)
.Select(file => Path.GetDirectoryName(file.Path)!)
.Distinct(StringComparer.OrdinalIgnoreCase))
{
@@ -135,7 +146,9 @@ private static FolderPluginCatalogOptions CreateCatalogOptions(
IncludeSubfolders = includeSubfolders,
SearchPatterns = [.. baseOptions.SearchPatterns],
TypeFinderOptions = baseOptions.TypeFinderOptions,
- PluginNameOptions = baseOptions.PluginNameOptions,
+ PluginNameOptions = CreatePluginNameOptions(
+ baseOptions.PluginNameOptions,
+ satelliteAssemblies),
PluginLoadContextOptions = new PluginLoadContextOptions
{
UseHostApplicationAssemblies = baseLoadOptions.UseHostApplicationAssemblies,
@@ -147,6 +160,104 @@ private static FolderPluginCatalogOptions CreateCatalogOptions(
};
}
+ private static PluginFile CreatePluginFile(string pluginDirectory, string path)
+ {
+ var isManaged = IsManagedAssembly(path);
+ return new PluginFile(
+ path,
+ Path.GetRelativePath(pluginDirectory, path),
+ isManaged,
+ isManaged ? TryGetAssemblyName(path) : null);
+ }
+
+ private static PluginNameOptions CreatePluginNameOptions(
+ PluginNameOptions baseOptions,
+ IReadOnlyDictionary satelliteAssemblies)
+ {
+ if (satelliteAssemblies.Count == 0)
+ {
+ return baseOptions;
+ }
+
+ var configuredContexts = new HashSet();
+ var contextLock = new object();
+
+ void EnsureSatelliteResolver(Type type)
+ {
+ var context = AssemblyLoadContext.GetLoadContext(type.Assembly);
+ if (context is null || context == AssemblyLoadContext.Default)
+ {
+ return;
+ }
+
+ lock (contextLock)
+ {
+ if (configuredContexts.Add(context))
+ {
+ context.Resolving += ResolveSatelliteAssembly;
+ }
+ }
+ }
+
+ Assembly? ResolveSatelliteAssembly(
+ AssemblyLoadContext context,
+ AssemblyName requestedAssembly)
+ {
+ if (string.IsNullOrWhiteSpace(requestedAssembly.Name)
+ || string.IsNullOrWhiteSpace(requestedAssembly.CultureName)
+ || !satelliteAssemblies.TryGetValue(
+ GetSatelliteKey(requestedAssembly.Name, requestedAssembly.CultureName),
+ out var satelliteAssembly)
+ || requestedAssembly.Version is not null
+ && satelliteAssembly.AssemblyName!.Version != requestedAssembly.Version)
+ {
+ return null;
+ }
+
+ return context.LoadFromAssemblyPath(satelliteAssembly.Path);
+ }
+
+ return new PluginNameOptions
+ {
+ PluginNameGenerator = (_, type) =>
+ {
+ EnsureSatelliteResolver(type);
+ return baseOptions.PluginNameGenerator(baseOptions, type);
+ },
+ PluginVersionGenerator = (_, type) =>
+ {
+ EnsureSatelliteResolver(type);
+ return baseOptions.PluginVersionGenerator(baseOptions, type);
+ },
+ PluginDescriptionGenerator = (_, type) =>
+ {
+ EnsureSatelliteResolver(type);
+ return baseOptions.PluginDescriptionGenerator(baseOptions, type);
+ },
+ PluginProductVersionGenerator = (_, type) =>
+ {
+ EnsureSatelliteResolver(type);
+ return baseOptions.PluginProductVersionGenerator(baseOptions, type);
+ },
+ };
+ }
+
+ private static AssemblyName? TryGetAssemblyName(string path)
+ {
+ try
+ {
+ return AssemblyName.GetAssemblyName(path);
+ }
+ catch (BadImageFormatException)
+ {
+ return null;
+ }
+ catch (FileLoadException)
+ {
+ return null;
+ }
+ }
+
private static bool IsManagedAssembly(string path)
{
try
@@ -171,6 +282,9 @@ private static int GetRuntimeAssetPriority(string relativePath)
private static string GetHintKey(string fileName, bool isNative)
=> $"{(isNative ? 'N' : 'M')}:{fileName}";
+ private static string GetSatelliteKey(string assemblyName, string cultureName)
+ => $"{cultureName}:{assemblyName}";
+
internal static void SynchronizePluginFiles(string source, string destination)
{
Directory.CreateDirectory(destination);
@@ -322,5 +436,13 @@ private static int GetPathDepth(string path)
character == Path.DirectorySeparatorChar
|| character == Path.AltDirectorySeparatorChar);
- private sealed record PluginFile(string Path, string RelativePath, bool IsManaged);
+ private sealed record PluginFile(
+ string Path,
+ string RelativePath,
+ bool IsManaged,
+ AssemblyName? AssemblyName)
+ {
+ public bool IsSatelliteAssembly
+ => !string.IsNullOrWhiteSpace(this.AssemblyName?.CultureName);
+ }
}
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index 649d9f2c..81830e2f 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -56,6 +56,16 @@ public async Task LoadAsync(CancellationToken cancellationToken = default)
var installed = await this.nugetService.GetInstalledPackagesAsync(cancellationToken).ConfigureAwait(true);
var installedDict = installed.ToDictionary(p => p.Id, StringComparer.OrdinalIgnoreCase);
+ this.Packages.Clear();
+ foreach (var inst in installed)
+ {
+ this.Packages.Add(new PluginPackageViewModel(
+ new NuGetPackageInfo(inst.Id, inst.Version, inst.Id, string.Empty, string.Empty, null, null),
+ isInstalled: true,
+ installedVersion: inst.Version,
+ isUpdateAvailable: false));
+ }
+
var packages = await this.nugetService.SearchPackagesAsync(cancellationToken).ConfigureAwait(true);
this.logger.LogInformation("NuGetから{Count}件のプラグインパッケージを取得しました。", packages.Count);
diff --git a/docs/plugin.md b/docs/plugin.md
index a5644d0a..932ad583 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -141,7 +141,7 @@ NuGetパッケージで宣言されたランタイム依存関係も再帰的に
## 注意事項
-- プラグインは .NET 10 以上をターゲットにしてください
+- プラグインは WindowTranslator と同じ `net10.0` をターゲットにしてください。`net11.0` など、ホストより新しいTFMは読み込めません
- `true` を必ず設定してください
- ホスト側で既に提供されているパッケージは `ExcludeAssets="runtime"` を設定し、DLL を重複させないようにしてください
- 通常のランタイム依存は `PackageReference` として宣言してください
From 51cd4ea24c835d70241414ba6abd820d297b605a Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sun, 2 Aug 2026 03:30:49 +0900
Subject: [PATCH 10/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=81=AE=E3=83=97=E3=83=AC=E3=83=AA=E3=83=AA=E3=83=BC?=
=?UTF-8?q?=E3=82=B9=E5=B0=8E=E5=85=A5=E3=81=AB=E5=AF=BE=E5=BF=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../NuGetPluginServiceTests.cs | 134 ++++++++++++++++++
.../Modules/PluginStore/NuGetPluginService.cs | 19 ++-
.../Modules/PluginStore/PluginStoreView.xaml | 17 ++-
.../PluginStore/PluginStoreViewModel.cs | 110 ++++++++++----
.../Modules/Settings/AllSettingsDialog.xaml | 8 +-
.../Properties/Resources.Designer.cs | 7 +-
WindowTranslator/Properties/Resources.ar.resx | 5 +-
WindowTranslator/Properties/Resources.cs.resx | 5 +-
WindowTranslator/Properties/Resources.de.resx | 5 +-
WindowTranslator/Properties/Resources.en.resx | 5 +-
WindowTranslator/Properties/Resources.es.resx | 5 +-
WindowTranslator/Properties/Resources.fa.resx | 5 +-
.../Properties/Resources.fil.resx | 5 +-
WindowTranslator/Properties/Resources.fr.resx | 5 +-
WindowTranslator/Properties/Resources.hi.resx | 5 +-
WindowTranslator/Properties/Resources.hu.resx | 5 +-
WindowTranslator/Properties/Resources.id.resx | 5 +-
WindowTranslator/Properties/Resources.ko.resx | 5 +-
WindowTranslator/Properties/Resources.ms.resx | 5 +-
WindowTranslator/Properties/Resources.pl.resx | 5 +-
.../Properties/Resources.pt-BR.resx | 5 +-
WindowTranslator/Properties/Resources.resx | 5 +-
WindowTranslator/Properties/Resources.ru.resx | 5 +-
WindowTranslator/Properties/Resources.th.resx | 5 +-
WindowTranslator/Properties/Resources.tr.resx | 5 +-
WindowTranslator/Properties/Resources.vi.resx | 5 +-
.../Properties/Resources.zh-CN.resx | 5 +-
.../Properties/Resources.zh-TW.resx | 5 +-
docs/plugin.md | 3 +-
29 files changed, 343 insertions(+), 65 deletions(-)
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index 2eabbf91..91148cda 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -445,6 +445,102 @@ [new InstalledPackageInfo("Installed.Plugin", "1.2.3")])),
}
}
+ [Fact]
+ public async Task SearchReturnsReleaseAndPrereleaseVersions()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler
+ {
+ SearchResponseJson = """
+ {
+ "totalHits": 1,
+ "data": [
+ {
+ "id": "Test.Plugin",
+ "version": "1.1.0-beta.2",
+ "title": "Test Plugin",
+ "description": "Test description",
+ "authors": ["WindowTranslator.Tests"],
+ "versions": [
+ { "version": "1.0.0" },
+ { "version": "1.1.0-beta.1" },
+ { "version": "1.1.0-beta.2" }
+ ]
+ }
+ ]
+ }
+ """,
+ };
+ using var client = new HttpClient(handler);
+ using var service = CreateService(client, testDirectory);
+
+ var package = Assert.Single(await service.SearchPackagesAsync());
+
+ Assert.Equal("Test.Plugin", package.Id);
+ Assert.Equal("1.1.0-beta.2", package.Version);
+ Assert.Equal(
+ ["1.0.0", "1.1.0-beta.1", "1.1.0-beta.2"],
+ package.Versions);
+ Assert.Contains(
+ handler.RequestedUris,
+ uri => uri.Contains("prerelease=true", StringComparison.OrdinalIgnoreCase));
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public void PackageVersionSelectionRequiresOptInForPrerelease()
+ {
+ var package = new PluginPackageViewModel(
+ new NuGetPackageInfo(
+ "Test.Plugin",
+ "1.1.0-beta.2",
+ "Test Plugin",
+ string.Empty,
+ string.Empty,
+ null,
+ null,
+ ["1.0.0", "1.1.0-beta.1", "1.1.0-beta.2"]),
+ isInstalled: true,
+ installedVersion: "1.0.0");
+
+ Assert.Equal("1.0.0", package.LatestVersion);
+ Assert.False(package.UsePrerelease);
+ Assert.False(package.IsUpdateAvailable);
+
+ package.UsePrerelease = true;
+
+ Assert.Equal("1.1.0-beta.2", package.LatestVersion);
+ Assert.True(package.IsUpdateAvailable);
+ Assert.True(package.CanInstall);
+
+ var prereleaseOnlyPackage = new PluginPackageViewModel(
+ new NuGetPackageInfo(
+ "Preview.Plugin",
+ "2.0.0-preview.1",
+ "Preview Plugin",
+ string.Empty,
+ string.Empty,
+ null,
+ null,
+ ["2.0.0-preview.1"]),
+ isInstalled: false,
+ installedVersion: null);
+
+ Assert.Null(prereleaseOnlyPackage.LatestVersion);
+ Assert.False(prereleaseOnlyPackage.CanInstall);
+
+ prereleaseOnlyPackage.UsePrerelease = true;
+
+ Assert.Equal("2.0.0-preview.1", prereleaseOnlyPackage.LatestVersion);
+ Assert.True(prereleaseOnlyPackage.CanInstall);
+ }
+
[Fact]
public async Task InstallRejectsPackageRequiringNewerHostAbstractions()
{
@@ -931,6 +1027,10 @@ private sealed class InMemoryNuGetHandler : HttpMessageHandler
public List RequestedPaths { get; } = [];
+ public List RequestedUris { get; } = [];
+
+ public string? SearchResponseJson { get; init; }
+
public void AddPackage(string id, string version, byte[] package)
=> this.packages[(id.ToLowerInvariant(), version.ToLowerInvariant())] = package;
@@ -940,6 +1040,40 @@ protected override Task SendAsync(
{
var path = request.RequestUri!.AbsolutePath;
this.RequestedPaths.Add(path);
+ this.RequestedUris.Add(request.RequestUri.PathAndQuery);
+
+ if (path.Equals("/v3/index.json", StringComparison.OrdinalIgnoreCase))
+ {
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(
+ """
+ {
+ "resources": [
+ {
+ "@id": "https://nuget.test/query",
+ "@type": "SearchQueryService/3.5.0"
+ }
+ ]
+ }
+ """,
+ Encoding.UTF8,
+ "application/json"),
+ });
+ }
+
+ if (path.Equals("/query", StringComparison.OrdinalIgnoreCase)
+ && this.SearchResponseJson is not null)
+ {
+ return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
+ {
+ Content = new StringContent(
+ this.SearchResponseJson,
+ Encoding.UTF8,
+ "application/json"),
+ });
+ }
+
var segments = path.Trim('/').Split('/');
if (segments.Length == 3
&& segments[0].Equals("v3-flatcontainer", StringComparison.OrdinalIgnoreCase)
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 1d8b08ba..b5b84126 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -67,7 +67,7 @@ public async Task> SearchPackagesAsync(Cancellat
this.searchUrl = await GetSearchUrlAsync(cancellationToken).ConfigureAwait(false);
}
- var url = $"{this.searchUrl}?q=tags:{PluginTag}&take=100&semVerLevel=2.0.0&prerelease=false";
+ var url = $"{this.searchUrl}?q=tags:{PluginTag}&take=100&semVerLevel=2.0.0&prerelease=true";
this.logger.LogDebug("NuGet検索URL: {Url}", url);
using var response = await this.httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
@@ -86,7 +86,12 @@ public async Task> SearchPackagesAsync(Cancellat
Description: d.Description ?? string.Empty,
Authors: string.Join(", ", d.Authors ?? []),
ProjectUrl: d.ProjectUrl,
- LicenseUrl: d.LicenseUrl
+ LicenseUrl: d.LicenseUrl,
+ Versions: d.Versions?
+ .Select(version => version.Version)
+ .Where(version => !string.IsNullOrWhiteSpace(version))
+ .Select(version => version!)
+ .ToArray()
)).ToArray() ?? [];
}
@@ -457,7 +462,8 @@ public record NuGetPackageInfo(
string Description,
string Authors,
string? ProjectUrl,
- string? LicenseUrl
+ string? LicenseUrl,
+ IReadOnlyList? Versions = null
);
/// インストール済みパッケージ情報
@@ -491,5 +497,10 @@ internal record NuGetSearchData(
[property: JsonPropertyName("description")] string? Description,
[property: JsonPropertyName("authors")] string[]? Authors,
[property: JsonPropertyName("projectUrl")] string? ProjectUrl,
- [property: JsonPropertyName("licenseUrl")] string? LicenseUrl
+ [property: JsonPropertyName("licenseUrl")] string? LicenseUrl,
+ [property: JsonPropertyName("versions")] NuGetSearchVersion[]? Versions
+);
+
+internal record NuGetSearchVersion(
+ [property: JsonPropertyName("version")] string? Version
);
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
index 4b26ed4b..e86de86d 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
@@ -69,6 +69,7 @@
+
@@ -100,10 +101,20 @@
Text="{Binding StatusText}"
TextTrimming="CharacterEllipsis" />
+
+
@@ -122,7 +133,7 @@
CommandParameter="{Binding}"
Content="{x:Static properties:Resources.Install}"
Icon="{ui:SymbolIcon ArrowDownload24}"
- IsEnabled="{Binding IsInstalling, Converter={x:Static local:InverseBoolConverter.Default}}"
+ IsEnabled="{Binding CanInstall}"
Style="{StaticResource InstallButtonStyle}"
Visibility="{Binding IsInstalled, Converter={x:Static local:InverseBoolConverter.Default}, ConverterParameter=Visibility}" />
@@ -132,7 +143,7 @@
CommandParameter="{Binding}"
Content="{x:Static properties:Resources.Update}"
Icon="{ui:SymbolIcon ArrowSync24}"
- IsEnabled="{Binding IsInstalling, Converter={x:Static local:InverseBoolConverter.Default}}"
+ IsEnabled="{Binding CanInstall}"
Style="{StaticResource InstallButtonStyle}"
Visibility="{Binding IsUpdateAvailable, Converter={StaticResource b2vConv}}" />
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index 81830e2f..f1ca6f1a 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -62,8 +62,7 @@ public async Task LoadAsync(CancellationToken cancellationToken = default)
this.Packages.Add(new PluginPackageViewModel(
new NuGetPackageInfo(inst.Id, inst.Version, inst.Id, string.Empty, string.Empty, null, null),
isInstalled: true,
- installedVersion: inst.Version,
- isUpdateAvailable: false));
+ installedVersion: inst.Version));
}
var packages = await this.nugetService.SearchPackagesAsync(cancellationToken).ConfigureAwait(true);
@@ -75,11 +74,7 @@ public async Task LoadAsync(CancellationToken cancellationToken = default)
installedDict.TryGetValue(pkg.Id, out var installedInfo);
var isInstalled = installedInfo is not null;
var installedVersion = installedInfo?.Version;
- var isUpdateAvailable = isInstalled
- && installedVersion is not null
- && IsNewerVersion(pkg.Version, installedVersion);
-
- this.Packages.Add(new PluginPackageViewModel(pkg, isInstalled, installedVersion, isUpdateAvailable));
+ this.Packages.Add(new PluginPackageViewModel(pkg, isInstalled, installedVersion));
}
// インストール済みだがNuGetに見つからないパッケージも表示
@@ -90,8 +85,7 @@ public async Task LoadAsync(CancellationToken cancellationToken = default)
this.Packages.Add(new PluginPackageViewModel(
new NuGetPackageInfo(inst.Id, inst.Version, inst.Id, string.Empty, string.Empty, null, null),
isInstalled: true,
- installedVersion: inst.Version,
- isUpdateAvailable: false));
+ installedVersion: inst.Version));
}
}
}
@@ -118,20 +112,25 @@ public async Task InstallAsync(
PluginPackageViewModel package,
CancellationToken cancellationToken = default)
{
+ var version = package.LatestVersion;
+ if (string.IsNullOrWhiteSpace(version))
+ {
+ return;
+ }
+
package.IsInstalling = true;
try
{
- this.logger.LogInformation("プラグインのインストール開始: {PackageId} {Version}", package.Id, package.LatestVersion);
+ this.logger.LogInformation("プラグインのインストール開始: {PackageId} {Version}", package.Id, version);
var progress = new Progress(v => package.InstallProgress = v);
await this.nugetService.InstallPackageAsync(
package.Id,
- package.LatestVersion,
+ version,
progress,
cancellationToken).ConfigureAwait(true);
package.IsInstalled = true;
- package.InstalledVersion = package.LatestVersion;
- package.IsUpdateAvailable = false;
+ package.InstalledVersion = version;
package.InstallProgress = 0;
this.logger.LogInformation("プラグインのインストール完了: {PackageId}", package.Id);
@@ -185,7 +184,6 @@ public async Task UninstallAsync(PluginPackageViewModel package)
await this.nugetService.UninstallPackageAsync(package.Id).ConfigureAwait(true);
package.IsInstalled = false;
package.InstalledVersion = null;
- package.IsUpdateAvailable = false;
await this.dialogService.ShowSimpleDialogAsync(new()
{
@@ -204,16 +202,6 @@ await this.dialogService.ShowAlertAsync(
}
}
- private static bool IsNewerVersion(string latestVersion, string installedVersion)
- {
- if (NuGetVersion.TryParse(latestVersion, out var latest)
- && NuGetVersion.TryParse(installedVersion, out var installed))
- {
- return latest > installed;
- }
-
- return string.Compare(latestVersion, installedVersion, StringComparison.OrdinalIgnoreCase) > 0;
- }
}
///
@@ -225,7 +213,13 @@ public partial class PluginPackageViewModel : ObservableObject
public string Title { get; }
public string Description { get; }
public string Authors { get; }
- public string LatestVersion { get; }
+ public string? ReleaseVersion { get; }
+ public string? PrereleaseVersion { get; }
+ public string? LatestVersion => this.UsePrerelease
+ ? this.PrereleaseVersion
+ : this.ReleaseVersion;
+ public bool HasPrereleaseVersion => this.PrereleaseVersion is not null;
+ public bool CanInstall => !this.IsInstalling && this.LatestVersion is not null;
public string? ProjectUrl { get; }
public string? LicenseUrl { get; }
@@ -241,16 +235,25 @@ public partial class PluginPackageViewModel : ObservableObject
private bool isUpdateAvailable;
[ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(CanInstall))]
private bool isInstalling;
[ObservableProperty]
private double installProgress;
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(LatestVersion))]
+ [NotifyPropertyChangedFor(nameof(CanInstall))]
+ [NotifyPropertyChangedFor(nameof(StatusText))]
+ private bool usePrerelease;
+
public string StatusText
{
get
{
- if (this.IsUpdateAvailable && this.InstalledVersion is not null)
+ if (this.IsUpdateAvailable
+ && this.InstalledVersion is not null
+ && this.LatestVersion is not null)
return string.Format(Properties.Resources.UpdateAvailableVersion, this.InstalledVersion, this.LatestVersion);
if (this.IsInstalled && this.InstalledVersion is not null)
return string.Format(Properties.Resources.InstalledVersion, this.InstalledVersion);
@@ -261,18 +264,65 @@ public string StatusText
public PluginPackageViewModel(
NuGetPackageInfo info,
bool isInstalled,
- string? installedVersion,
- bool isUpdateAvailable)
+ string? installedVersion)
{
+ var versions = new[] { info.Version }
+ .Concat(info.Versions ?? [])
+ .Where(version => !string.IsNullOrWhiteSpace(version))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Select(version => (Text: version, Parsed: ParseVersion(version)))
+ .Where(version => version.Parsed is not null)
+ .ToArray();
+
this.Id = info.Id;
this.Title = info.Title;
this.Description = info.Description;
this.Authors = info.Authors;
- this.LatestVersion = info.Version;
+ this.ReleaseVersion = versions
+ .Where(version => !version.Parsed!.IsPrerelease)
+ .OrderByDescending(version => version.Parsed)
+ .Select(version => version.Text)
+ .FirstOrDefault();
+ this.PrereleaseVersion = versions
+ .Where(version => version.Parsed!.IsPrerelease)
+ .OrderByDescending(version => version.Parsed)
+ .Select(version => version.Text)
+ .FirstOrDefault();
this.ProjectUrl = info.ProjectUrl;
this.LicenseUrl = info.LicenseUrl;
this.isInstalled = isInstalled;
this.installedVersion = installedVersion;
- this.isUpdateAvailable = isUpdateAvailable;
+ this.usePrerelease = this.PrereleaseVersion is not null
+ && NuGetVersion.TryParse(installedVersion, out var installed)
+ && installed.IsPrerelease;
+ RefreshUpdateAvailable();
+ }
+
+ partial void OnIsInstalledChanged(bool value) => RefreshUpdateAvailable();
+
+ partial void OnInstalledVersionChanged(string? value) => RefreshUpdateAvailable();
+
+ partial void OnUsePrereleaseChanged(bool value) => RefreshUpdateAvailable();
+
+ private void RefreshUpdateAvailable()
+ {
+ this.IsUpdateAvailable = this.IsInstalled
+ && this.InstalledVersion is not null
+ && this.LatestVersion is not null
+ && IsNewerVersion(this.LatestVersion, this.InstalledVersion);
+ }
+
+ private static NuGetVersion? ParseVersion(string version)
+ => NuGetVersion.TryParse(version, out var parsed) ? parsed : null;
+
+ private static bool IsNewerVersion(string latestVersion, string installedVersion)
+ {
+ if (NuGetVersion.TryParse(latestVersion, out var latest)
+ && NuGetVersion.TryParse(installedVersion, out var installed))
+ {
+ return latest > installed;
+ }
+
+ return string.Compare(latestVersion, installedVersion, StringComparison.OrdinalIgnoreCase) > 0;
}
}
diff --git a/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml b/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
index 8c2eb942..00d0b35c 100644
--- a/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
+++ b/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
@@ -375,6 +375,9 @@
+
+
+
-
-
-
-
\ No newline at end of file
+
diff --git a/WindowTranslator/Properties/Resources.Designer.cs b/WindowTranslator/Properties/Resources.Designer.cs
index 30f17809..b4e15bb9 100644
--- a/WindowTranslator/Properties/Resources.Designer.cs
+++ b/WindowTranslator/Properties/Resources.Designer.cs
@@ -458,10 +458,15 @@ internal Resources() {
public static string PluginInstallSuccess => ResourceManager.GetString("PluginInstallSuccess", resourceCulture) ?? string.Empty;
///
- /// "プラグインストア" に類似しているローカライズされた文字列を検索します。
+ /// "プラグイン" に類似しているローカライズされた文字列を検索します。
///
public static string PluginStore => ResourceManager.GetString("PluginStore", resourceCulture) ?? string.Empty;
+ ///
+ /// "プレリリース" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string Prerelease => ResourceManager.GetString("Prerelease", resourceCulture) ?? string.Empty;
+
///
/// "プロジェクトページ" に類似しているローカライズされた文字列を検索します。
///
diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx
index 1d0e33af..1de815fd 100644
--- a/WindowTranslator/Properties/Resources.ar.resx
+++ b/WindowTranslator/Properties/Resources.ar.resx
@@ -451,7 +451,10 @@
- متجر المكونات الإضافية
+ المكونات الإضافية
+
+
+ إصدار أولي
تثبيت
diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx
index 7438924c..cd8b7a45 100644
--- a/WindowTranslator/Properties/Resources.cs.resx
+++ b/WindowTranslator/Properties/Resources.cs.resx
@@ -341,7 +341,10 @@ Monitory nejsou podporovány.
- Obchod s pluginy
+ Pluginy
+
+
+ Předběžná verze
Nainstalovat
diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx
index dbdf0186..410b25d5 100644
--- a/WindowTranslator/Properties/Resources.de.resx
+++ b/WindowTranslator/Properties/Resources.de.resx
@@ -460,7 +460,10 @@ Monitore werden nicht unterstützt.
- Plugin-Store
+ Plugins
+
+
+ Vorabversion
Installieren
diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx
index 6d0acccf..23eed21d 100644
--- a/WindowTranslator/Properties/Resources.en.resx
+++ b/WindowTranslator/Properties/Resources.en.resx
@@ -460,7 +460,10 @@ Monitors are not supported.
- Plugin Store
+ Plugins
+
+
+ Prerelease
Install
diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx
index bf6977f4..1b36ad36 100644
--- a/WindowTranslator/Properties/Resources.es.resx
+++ b/WindowTranslator/Properties/Resources.es.resx
@@ -451,7 +451,10 @@
- Tienda de plugins
+ Plugins
+
+
+ Versión preliminar
Instalar
diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx
index a5bacfea..0be9545a 100644
--- a/WindowTranslator/Properties/Resources.fa.resx
+++ b/WindowTranslator/Properties/Resources.fa.resx
@@ -445,7 +445,10 @@
- فروشگاه افزونه
+ افزونهها
+
+
+ پیشانتشار
نصب
diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx
index 44c7b38d..5d860d57 100644
--- a/WindowTranslator/Properties/Resources.fil.resx
+++ b/WindowTranslator/Properties/Resources.fil.resx
@@ -460,7 +460,10 @@ Ang monitor ay hindi suportado.
- Plugin Store
+ Mga Plugin
+
+
+ Prerelease
I-install
diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx
index 2e988292..aac8ccbb 100644
--- a/WindowTranslator/Properties/Resources.fr.resx
+++ b/WindowTranslator/Properties/Resources.fr.resx
@@ -451,7 +451,10 @@
- Boutique de plugins
+ Plugins
+
+
+ Préversion
Installer
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index 0d475cbd..469aa170 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -453,7 +453,10 @@
- प्लगइन स्टोर
+ प्लगइन
+
+
+ प्रीरिलीज़
इंस्टॉल करें
diff --git a/WindowTranslator/Properties/Resources.hu.resx b/WindowTranslator/Properties/Resources.hu.resx
index 3d6eb384..dcc6720c 100644
--- a/WindowTranslator/Properties/Resources.hu.resx
+++ b/WindowTranslator/Properties/Resources.hu.resx
@@ -341,7 +341,10 @@ A monitorok nem támogatottak.
- Bővítményáruház
+ Bővítmények
+
+
+ Előzetes kiadás
Telepítés
diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx
index 1e5935b6..aa7140fc 100644
--- a/WindowTranslator/Properties/Resources.id.resx
+++ b/WindowTranslator/Properties/Resources.id.resx
@@ -459,7 +459,10 @@ Monitor tidak didukung.
- Toko Plugin
+ Plugin
+
+
+ Prarilis
Pasang
diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx
index 6843817f..817dc5d9 100644
--- a/WindowTranslator/Properties/Resources.ko.resx
+++ b/WindowTranslator/Properties/Resources.ko.resx
@@ -460,7 +460,10 @@
- 플러그인 스토어
+ 플러그인
+
+
+ 시험판
설치
diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx
index 35fcf87a..468b4d13 100644
--- a/WindowTranslator/Properties/Resources.ms.resx
+++ b/WindowTranslator/Properties/Resources.ms.resx
@@ -459,7 +459,10 @@ Monitor tidak disokong.
- Kedai Plugin
+ Plugin
+
+
+ Prakeluaran
Pasang
diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx
index a7f2a347..f667ae1c 100644
--- a/WindowTranslator/Properties/Resources.pl.resx
+++ b/WindowTranslator/Properties/Resources.pl.resx
@@ -460,7 +460,10 @@ Monitory nie są obsługiwane.
- Sklep wtyczek
+ Wtyczki
+
+
+ Wersja wstępna
Zainstaluj
diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx
index 57e5427a..2fbacce7 100644
--- a/WindowTranslator/Properties/Resources.pt-BR.resx
+++ b/WindowTranslator/Properties/Resources.pt-BR.resx
@@ -459,7 +459,10 @@ Monitor tidak didukung.
- Loja de Plugins
+ Plugins
+
+
+ Pré-lançamento
Instalar
diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx
index 098a8db5..e9e8b24f 100644
--- a/WindowTranslator/Properties/Resources.resx
+++ b/WindowTranslator/Properties/Resources.resx
@@ -460,7 +460,10 @@
- プラグインストア
+ プラグイン
+
+
+ プレリリース
インストール
diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx
index e6c2f57b..6fddcfb1 100644
--- a/WindowTranslator/Properties/Resources.ru.resx
+++ b/WindowTranslator/Properties/Resources.ru.resx
@@ -451,7 +451,10 @@
- Магазин плагинов
+ Плагины
+
+
+ Предварительная версия
Установить
diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx
index 32b18f69..17bebcad 100644
--- a/WindowTranslator/Properties/Resources.th.resx
+++ b/WindowTranslator/Properties/Resources.th.resx
@@ -460,7 +460,10 @@
- ร้านปลั๊กอิน
+ ปลั๊กอิน
+
+
+ รุ่นก่อนเผยแพร่
ติดตั้ง
diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx
index 14d89982..3a192391 100644
--- a/WindowTranslator/Properties/Resources.tr.resx
+++ b/WindowTranslator/Properties/Resources.tr.resx
@@ -460,7 +460,10 @@ Monitör desteklenmiyor.
- Eklenti Mağazası
+ Eklentiler
+
+
+ Ön sürüm
Yükle
diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx
index 1e8d917e..878151c4 100644
--- a/WindowTranslator/Properties/Resources.vi.resx
+++ b/WindowTranslator/Properties/Resources.vi.resx
@@ -460,7 +460,10 @@ Màn hình không được hỗ trợ.
- Cửa hàng Plugin
+ Plugin
+
+
+ Bản phát hành trước
Cài đặt
diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx
index 9302d7c2..04b14e05 100644
--- a/WindowTranslator/Properties/Resources.zh-CN.resx
+++ b/WindowTranslator/Properties/Resources.zh-CN.resx
@@ -460,7 +460,10 @@
- 插件商店
+ 插件
+
+
+ 预发行版
安装
diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx
index f6e61784..da6427bd 100644
--- a/WindowTranslator/Properties/Resources.zh-TW.resx
+++ b/WindowTranslator/Properties/Resources.zh-TW.resx
@@ -460,7 +460,10 @@
- 外掛程式商店
+ 外掛程式
+
+
+ 預發行版本
安裝
diff --git a/docs/plugin.md b/docs/plugin.md
index 932ad583..942a4f6f 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -124,8 +124,9 @@ public class MyTranslateModule : ITranslateModule { ... }
## アプリからインストールする
1. WindowTranslator の設定を開きます。
-2. 「プラグインストア」タブを選択します。
+2. 「プラグイン」タブを選択します。
3. 利用するプラグインの「インストール」を選択します。
+ プレリリース版を利用する場合は、そのプラグインの「プレリリース」にチェックを入れます。
4. インストール完了後に WindowTranslator を再起動します。
NuGetパッケージで宣言されたランタイム依存関係も再帰的に取得されます。
From 9e597f2bae79de38de675919813a5e19b12c48c5 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sun, 2 Aug 2026 03:52:53 +0900
Subject: [PATCH 11/43] =?UTF-8?q?NuGet=E3=83=91=E3=83=83=E3=82=B1=E3=83=BC?=
=?UTF-8?q?=E3=82=B8=E3=81=AE=E7=89=88=E7=95=AA=E5=8F=B7=E3=82=92=E3=82=BF?=
=?UTF-8?q?=E3=82=B0=E3=81=8B=E3=82=89=E5=8F=96=E5=BE=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/dotnet-package.yml | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/dotnet-package.yml b/.github/workflows/dotnet-package.yml
index 685cafa9..d8c7082d 100644
--- a/.github/workflows/dotnet-package.yml
+++ b/.github/workflows/dotnet-package.yml
@@ -25,12 +25,21 @@ jobs:
versionSpec: "6.x"
- id: gitversion
uses: gittools/actions/gitversion/execute@v4.7.0
+ - id: package-version
+ shell: pwsh
+ run: |
+ $tag = '${{ github.ref_name }}'
+ if ($tag -notmatch '^v(?[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)$') {
+ Write-Error "NuGet package tag must be v-prefixed SemVer: $tag"
+ exit 1
+ }
+ "version=$($Matches.version)" >> $env:GITHUB_OUTPUT
- uses: Jimver/cuda-toolkit@v0.2.30
with:
cuda: '12.9.0'
- run: |
dotnet pack WindowTranslator.Abstractions -c Release -o pack `
- -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
+ -p:Version=${{ steps.package-version.outputs.version }} `
-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
@@ -38,7 +47,7 @@ jobs:
exit $LASTEXITCODE
}
dotnet pack ColorThief\ColorThief -c Release -o pack `
- -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
+ -p:Version=${{ steps.package-version.outputs.version }} `
-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
@@ -58,7 +67,7 @@ jobs:
continue
}
dotnet pack $project.FullName -c Release -o pack `
- -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
+ -p:Version=${{ steps.package-version.outputs.version }} `
-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
From d7dc5f44ac07c8327a9454772203b193172f9a4c Mon Sep 17 00:00:00 2001
From: Freesia
Date: Mon, 3 Aug 2026 09:34:30 +0900
Subject: [PATCH 12/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=82=B9=E3=83=88=E3=82=A2=E3=81=AENuGet=E9=80=A3?=
=?UTF-8?q?=E6=90=BA=E3=82=92=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9=E3=83=AA?=
=?UTF-8?q?=E3=81=B8=E7=A7=BB=E8=A1=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Directory.Packages.props | 2 +
Plugins/Directory.Build.targets | 12 +
.../README.md | 22 +
.../README.md | 17 +
.../README.md | 22 +
.../README.md | 25 +
.../README.md | 24 +
.../README.md | 27 +
.../README.md | 21 +
.../README.md | 28 +
.../README.md | 25 +-
.../README.md | 25 +
.../README.md | 22 +
.../NuGetPluginServiceTests.cs | 520 +++++++++++++++---
.../PluginStore/NuGetPackageInstaller.cs | 173 +++---
.../Modules/PluginStore/NuGetPluginService.cs | 207 ++++---
.../NuGetProtocolPluginMetadataSource.cs | 122 ++++
.../Modules/PluginStore/PluginStoreView.xaml | 184 ++++---
.../PluginStore/PluginStoreViewModel.cs | 112 +++-
WindowTranslator/WindowTranslator.csproj | 2 +
20 files changed, 1268 insertions(+), 324 deletions(-)
create mode 100644 Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/README.md
create mode 100644 Plugins/WindowTranslator.Plugin.ColorThiefPlugin/README.md
create mode 100644 Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/README.md
create mode 100644 Plugins/WindowTranslator.Plugin.FoMPlugin/README.md
create mode 100644 Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/README.md
create mode 100644 Plugins/WindowTranslator.Plugin.GoogleAIPlugin/README.md
create mode 100644 Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/README.md
create mode 100644 Plugins/WindowTranslator.Plugin.LLMPlugin/README.md
create mode 100644 Plugins/WindowTranslator.Plugin.PLaMoPlugin/README.md
create mode 100644 Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/README.md
create mode 100644 WindowTranslator/Modules/PluginStore/NuGetProtocolPluginMetadataSource.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index bf4b2b97..26c77095 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -35,6 +35,8 @@
+
+
diff --git a/Plugins/Directory.Build.targets b/Plugins/Directory.Build.targets
index f880e7be..ae80e3ff 100644
--- a/Plugins/Directory.Build.targets
+++ b/Plugins/Directory.Build.targets
@@ -4,10 +4,22 @@
$(PackageTags);windowtranslator-plugin
+ README.md
+ $(TargetsForTfmSpecificContentInPackage);AddPluginReadmeToPackage
$(TargetsForTfmSpecificBuildOutput);AddPluginRuntimeAssetsToPackage
+
+
+
+
+ README.md
+
+
+
+
-
-
-
-
-
+ DataContext="{Binding SelectedPackage}">
+
+
+
+
+
+
+
+
-
+
+
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
-
+
+
+
-
-
+
-
-
-
+
+
+
+
+
+
+
+
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index f1ca6f1a..fb9da678 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
+using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
@@ -17,6 +18,8 @@ public partial class PluginStoreViewModel : ObservableObject
private readonly NuGetPluginService nugetService;
private readonly ILogger logger;
private readonly IContentDialogService dialogService;
+ private CancellationTokenSource? readmeLoadCancellation;
+ private PluginPackageViewModel? selectedPackage;
[ObservableProperty]
private bool isLoading;
@@ -24,8 +27,34 @@ public partial class PluginStoreViewModel : ObservableObject
[ObservableProperty]
private string? errorMessage;
- [ObservableProperty]
- private PluginPackageViewModel? selectedPackage;
+ public PluginPackageViewModel? SelectedPackage
+ {
+ get => this.selectedPackage;
+ set
+ {
+ var previous = this.selectedPackage;
+ if (!SetProperty(ref this.selectedPackage, value))
+ {
+ return;
+ }
+
+ if (previous is not null)
+ {
+ previous.PropertyChanged -= OnSelectedPackagePropertyChanged;
+ previous.IsReadmeLoading = false;
+ }
+ if (value is not null)
+ {
+ value.PropertyChanged += OnSelectedPackagePropertyChanged;
+ StartReadmeLoad(value);
+ }
+ else
+ {
+ this.readmeLoadCancellation?.Cancel();
+ this.readmeLoadCancellation = null;
+ }
+ }
+ }
public ObservableCollection Packages { get; } = [];
@@ -202,6 +231,76 @@ await this.dialogService.ShowAlertAsync(
}
}
+ private void OnSelectedPackagePropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (sender is PluginPackageViewModel package
+ && ReferenceEquals(package, this.SelectedPackage)
+ && e.PropertyName == nameof(PluginPackageViewModel.LatestVersion))
+ {
+ StartReadmeLoad(package);
+ }
+ }
+
+ private void StartReadmeLoad(PluginPackageViewModel package)
+ {
+ this.readmeLoadCancellation?.Cancel();
+ this.readmeLoadCancellation = null;
+ package.ReadmeMarkdown = null;
+
+ var version = package.LatestVersion;
+ if (string.IsNullOrWhiteSpace(version))
+ {
+ package.IsReadmeLoading = false;
+ return;
+ }
+
+ var cancellationSource = new CancellationTokenSource();
+ this.readmeLoadCancellation = cancellationSource;
+ package.IsReadmeLoading = true;
+ _ = LoadPackageReadmeAsync(package, version, cancellationSource);
+ }
+
+ private async Task LoadPackageReadmeAsync(
+ PluginPackageViewModel package,
+ string version,
+ CancellationTokenSource cancellationSource)
+ {
+ try
+ {
+ var readme = await this.nugetService.GetPackageReadmeAsync(
+ package.Id,
+ version,
+ cancellationSource.Token).ConfigureAwait(true);
+ if (!cancellationSource.IsCancellationRequested
+ && ReferenceEquals(package, this.SelectedPackage)
+ && string.Equals(version, package.LatestVersion, StringComparison.OrdinalIgnoreCase))
+ {
+ package.ReadmeMarkdown = readme;
+ }
+ }
+ catch (OperationCanceledException) when (cancellationSource.IsCancellationRequested)
+ {
+ // 選択変更によるキャンセルは正常
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogWarning(
+ ex,
+ "プラグインREADMEの取得に失敗しました: {PackageId} {Version}",
+ package.Id,
+ version);
+ }
+ finally
+ {
+ if (ReferenceEquals(this.readmeLoadCancellation, cancellationSource))
+ {
+ package.IsReadmeLoading = false;
+ this.readmeLoadCancellation = null;
+ }
+ cancellationSource.Dispose();
+ }
+ }
+
}
///
@@ -241,12 +340,21 @@ public partial class PluginPackageViewModel : ObservableObject
[ObservableProperty]
private double installProgress;
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(HasReadme))]
+ private string? readmeMarkdown;
+
+ [ObservableProperty]
+ private bool isReadmeLoading;
+
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(LatestVersion))]
[NotifyPropertyChangedFor(nameof(CanInstall))]
[NotifyPropertyChangedFor(nameof(StatusText))]
private bool usePrerelease;
+ public bool HasReadme => !string.IsNullOrWhiteSpace(this.ReadmeMarkdown);
+
public string StatusText
{
get
diff --git a/WindowTranslator/WindowTranslator.csproj b/WindowTranslator/WindowTranslator.csproj
index f97b6cea..dd54bcec 100644
--- a/WindowTranslator/WindowTranslator.csproj
+++ b/WindowTranslator/WindowTranslator.csproj
@@ -52,6 +52,8 @@
+
+
From e6168a307992b172b8f270b03b825330778040f9 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Mon, 3 Aug 2026 22:52:43 +0900
Subject: [PATCH 13/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E6=83=85=E5=A0=B1=E3=81=AE=E3=83=90=E3=83=83=E3=82=AF?=
=?UTF-8?q?=E3=82=B0=E3=83=A9=E3=82=A6=E3=83=B3=E3=83=89=E6=9B=B4=E6=96=B0?=
=?UTF-8?q?=E3=81=A8=E5=86=8D=E8=B5=B7=E5=8B=95=E3=82=92=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../NuGetPluginServiceTests.cs | 223 ++++++++++++++++-
WindowTranslator/ApplicationRestart.cs | 82 ++++++
.../Modules/PluginStore/NuGetPluginCatalog.cs | 78 +++++-
.../Modules/PluginStore/NuGetPluginService.cs | 227 +++++++++++++++--
.../Modules/PluginStore/PluginStoreView.xaml | 2 +-
.../PluginStore/PluginStoreViewModel.cs | 236 ++++++++++++++----
.../Modules/Settings/AllSettingsViewModel.cs | 1 +
WindowTranslator/Program.cs | 6 +-
.../Properties/Resources.Designer.cs | 10 +
WindowTranslator/Properties/Resources.en.resx | 6 +
WindowTranslator/Properties/Resources.resx | 6 +
docs/plugin.md | 7 +-
12 files changed, 810 insertions(+), 74 deletions(-)
create mode 100644 WindowTranslator/ApplicationRestart.cs
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index 729e0570..9744f4ad 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -9,6 +9,7 @@
using System.Text;
using System.Text.Json;
using System.Xml.Linq;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
using NuGet.Frameworks;
using NuGet.Packaging;
@@ -73,7 +74,7 @@ public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories
}));
using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory);
+ using var service = CreateService(client, testDirectory, hostMajorVersion: 7);
await service.InstallPackageAsync("Root.Plugin", "1.0.0");
@@ -112,6 +113,8 @@ await File.ReadAllTextAsync(Path.Combine(
var package = Assert.Single(installed);
Assert.Equal("Root.Plugin", package.Id);
Assert.Equal("1.0.0", package.Version);
+ Assert.Equal(7, package.HostMajorVersion);
+ Assert.True(package.IsCompatible);
}
finally
{
@@ -413,6 +416,126 @@ await File.WriteAllTextAsync(
}
}
+ [Fact]
+ public async Task ExistingManifestRecordsCurrentHostMajorVersionOnFirstRead()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(testDirectory, "nuget-manifest.json"),
+ JsonSerializer.Serialize(new InstalledManifest(
+ [new InstalledPackageInfo("Legacy.Plugin", "1.0.0")])));
+ using var handler = new InMemoryNuGetHandler();
+ using var client = new HttpClient(handler);
+ using var service = CreateService(
+ client,
+ testDirectory,
+ hostMajorVersion: 7);
+
+ var package = Assert.Single(await service.GetInstalledPackagesAsync());
+
+ Assert.Equal(7, package.HostMajorVersion);
+ Assert.True(package.IsCompatible);
+ using var document = JsonDocument.Parse(await File.ReadAllTextAsync(
+ Path.Combine(testDirectory, "nuget-manifest.json")));
+ Assert.Equal(
+ 7,
+ document.RootElement
+ .GetProperty("Packages")[0]
+ .GetProperty("HostMajorVersion")
+ .GetInt32());
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task InstalledPackageFromAnotherHostMajorVersionIsMarkedIncompatible()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(testDirectory, "nuget-manifest.json"),
+ JsonSerializer.Serialize(new InstalledManifest(
+ [new InstalledPackageInfo("Old.Plugin", "1.0.0", HostMajorVersion: 6)])));
+ using var handler = new InMemoryNuGetHandler();
+ using var client = new HttpClient(handler);
+ using var service = CreateService(
+ client,
+ testDirectory,
+ hostMajorVersion: 7);
+
+ var package = Assert.Single(await service.GetInstalledPackagesAsync());
+
+ Assert.False(package.IsCompatible);
+ Assert.Equal(6, package.HostMajorVersion);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task BackgroundServiceRefreshesPluginInformationWithoutOpeningSettings()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ var metadataSource = new InMemoryNuGetMetadataSource
+ {
+ SearchResults =
+ [
+ new NuGetPluginSearchMetadata(
+ "Background.Plugin",
+ "Background Plugin",
+ null,
+ null,
+ null,
+ null),
+ ],
+ };
+ metadataSource.AddVersions(
+ "Background.Plugin",
+ CreatePluginVersionMetadata("1.0.0"));
+ using var client = new HttpClient(handler);
+ using var service = CreateService(
+ client,
+ testDirectory,
+ metadataSource: metadataSource);
+ var updated = new TaskCompletionSource(
+ TaskCreationOptions.RunContinuationsAsynchronously);
+ service.PackageInformationUpdated += (_, _) => updated.TrySetResult();
+
+ Assert.IsAssignableFrom(service);
+ await service.StartAsync(CancellationToken.None);
+ try
+ {
+ await updated.Task.WaitAsync(TimeSpan.FromSeconds(5));
+ }
+ finally
+ {
+ await service.StopAsync(CancellationToken.None);
+ }
+
+ Assert.True(service.PackageSnapshot.IsInitialized);
+ Assert.Null(service.PackageSnapshot.Error);
+ Assert.Equal(
+ "Background.Plugin",
+ Assert.Single(service.PackageSnapshot.Packages).Id);
+ Assert.Equal(["windowtranslator-plugin"], metadataSource.RequestedTags);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
[Fact]
public async Task PluginStoreKeepsInstalledPackagesVisibleWhenNuGetSearchFails()
{
@@ -608,6 +731,49 @@ public void PackageVersionSelectionRequiresOptInForPrerelease()
Assert.True(prereleaseOnlyPackage.CanInstall);
}
+ [Fact]
+ public void IncompatibleInstalledPackageCanReinstallACompatibleVersion()
+ {
+ var package = new PluginPackageViewModel(
+ new NuGetPackageInfo(
+ "Test.Plugin",
+ "1.0.0",
+ "Test Plugin",
+ string.Empty,
+ string.Empty,
+ null,
+ null,
+ ["1.0.0"]),
+ isInstalled: true,
+ installedVersion: "1.0.0",
+ isCompatible: false,
+ hasCompatiblePackageVersion: true);
+
+ Assert.False(package.IsUpdateAvailable);
+ Assert.True(package.RequiresReinstall);
+ Assert.True(package.CanUpdate);
+ Assert.Equal(WindowTranslator.Properties.Resources.PluginIncompatible, package.StatusText);
+
+ package.IsCompatible = true;
+
+ Assert.False(package.RequiresReinstall);
+ Assert.False(package.CanUpdate);
+ }
+
+ [Fact]
+ public void RestartArgumentsAreNotForwardedToTheRestartedApplication()
+ {
+ var arguments = ApplicationRestart.RemoveRestartArguments(
+ [
+ "--IgnoreUpdate",
+ ApplicationRestart.RestartProcessIdArgument,
+ "1234",
+ "--SuppressMode",
+ ]);
+
+ Assert.Equal(["--IgnoreUpdate", "--SuppressMode"], arguments);
+ }
+
[Fact]
public async Task InstallRejectsPackageRequiringNewerHostAbstractions()
{
@@ -987,6 +1153,55 @@ public void CatalogSynchronizationClearsStaleFilesWhenSourceIsMissing()
}
}
+ [Fact]
+ public void CatalogSynchronizationExcludesPackagesFromAnotherHostMajorVersion()
+ {
+ var sourceDirectory = CreateTestDirectory();
+ var destinationDirectory = CreateTestDirectory();
+ try
+ {
+ var compatibleDirectory = Path.Combine(sourceDirectory, "Compatible.Plugin");
+ var incompatibleDirectory = Path.Combine(sourceDirectory, "Incompatible.Plugin");
+ Directory.CreateDirectory(compatibleDirectory);
+ Directory.CreateDirectory(incompatibleDirectory);
+ File.WriteAllText(Path.Combine(compatibleDirectory, "Compatible.Plugin.dll"), "compatible");
+ File.WriteAllText(Path.Combine(incompatibleDirectory, "Incompatible.Plugin.dll"), "incompatible");
+ Directory.CreateDirectory(Path.Combine(destinationDirectory, "Incompatible.Plugin"));
+ File.WriteAllText(
+ Path.Combine(destinationDirectory, "Incompatible.Plugin", "Incompatible.Plugin.dll"),
+ "stale");
+ File.WriteAllText(
+ Path.Combine(sourceDirectory, "nuget-manifest.json"),
+ JsonSerializer.Serialize(new InstalledManifest(
+ [
+ new InstalledPackageInfo("Compatible.Plugin", "1.0.0", HostMajorVersion: 7),
+ new InstalledPackageInfo("Incompatible.Plugin", "1.0.0", HostMajorVersion: 6),
+ ])));
+
+ var incompatiblePackages = NuGetPluginCatalog.GetIncompatiblePackageIds(
+ sourceDirectory,
+ hostMajorVersion: 7);
+ NuGetPluginCatalog.SynchronizePluginFiles(
+ sourceDirectory,
+ destinationDirectory,
+ incompatiblePackages);
+
+ Assert.True(File.Exists(Path.Combine(
+ destinationDirectory,
+ "Compatible.Plugin",
+ "Compatible.Plugin.dll")));
+ Assert.False(Directory.Exists(Path.Combine(
+ destinationDirectory,
+ "Incompatible.Plugin")));
+ Assert.Equal(["Incompatible.Plugin"], incompatiblePackages);
+ }
+ finally
+ {
+ DeleteTestDirectory(sourceDirectory);
+ DeleteTestDirectory(destinationDirectory);
+ }
+ }
+
[Fact]
public async Task CatalogLoadsARealAssemblyFromAPackageSubdirectory()
{
@@ -1161,13 +1376,15 @@ private static NuGetPluginService CreateService(
HttpClient client,
string pluginDirectory,
IReadOnlyDictionary? hostPackageVersions = null,
- INuGetPluginMetadataSource? metadataSource = null)
+ INuGetPluginMetadataSource? metadataSource = null,
+ int? hostMajorVersion = null)
=> new(
NullLogger.Instance,
client,
pluginDirectory,
hostPackageVersions: hostPackageVersions,
- metadataSource: metadataSource ?? new InMemoryNuGetMetadataSource());
+ metadataSource: metadataSource ?? new InMemoryNuGetMetadataSource(),
+ hostMajorVersion: hostMajorVersion);
private static NuGetPluginVersionMetadata CreatePluginVersionMetadata(
string version,
diff --git a/WindowTranslator/ApplicationRestart.cs b/WindowTranslator/ApplicationRestart.cs
new file mode 100644
index 00000000..5024ad09
--- /dev/null
+++ b/WindowTranslator/ApplicationRestart.cs
@@ -0,0 +1,82 @@
+using System.Diagnostics;
+using System.Globalization;
+using System.Windows;
+
+namespace WindowTranslator;
+
+internal static class ApplicationRestart
+{
+ internal const string RestartProcessIdArgument = "--windowtranslator-restart-pid";
+ private static readonly TimeSpan PreviousProcessWaitTimeout = TimeSpan.FromSeconds(30);
+
+ public static void Restart()
+ {
+ var executablePath = Environment.ProcessPath
+ ?? throw new InvalidOperationException("実行ファイルのパスを取得できませんでした。");
+ var startInfo = new ProcessStartInfo(executablePath)
+ {
+ UseShellExecute = false,
+ };
+ startInfo.ArgumentList.Add(RestartProcessIdArgument);
+ startInfo.ArgumentList.Add(Environment.ProcessId.ToString(CultureInfo.InvariantCulture));
+ foreach (var argument in RemoveRestartArguments(Environment.GetCommandLineArgs().Skip(1)))
+ {
+ startInfo.ArgumentList.Add(argument);
+ }
+
+ _ = Process.Start(startInfo)
+ ?? throw new InvalidOperationException("WindowTranslatorを再起動できませんでした。");
+ Application.Current.Shutdown();
+ }
+
+ public static string[] WaitForPreviousProcess(IEnumerable arguments)
+ {
+ var (remainingArguments, processId) = ParseRestartArguments(arguments);
+
+ if (processId is not null && processId != Environment.ProcessId)
+ {
+ try
+ {
+ using var process = Process.GetProcessById(processId.Value);
+ _ = process.WaitForExit(PreviousProcessWaitTimeout);
+ }
+ catch (ArgumentException)
+ {
+ // 再起動元のプロセスはすでに終了している
+ }
+ }
+
+ return remainingArguments;
+ }
+
+ internal static string[] RemoveRestartArguments(IEnumerable arguments)
+ => ParseRestartArguments(arguments).Arguments;
+
+ private static (string[] Arguments, int? ProcessId) ParseRestartArguments(
+ IEnumerable arguments)
+ {
+ var argumentArray = arguments.ToArray();
+ var remainingArguments = new List();
+ int? processId = null;
+ for (var index = 0; index < argumentArray.Length; index++)
+ {
+ var argument = argumentArray[index];
+ if (argument.Equals(RestartProcessIdArgument, StringComparison.OrdinalIgnoreCase)
+ && index + 1 < argumentArray.Length
+ && int.TryParse(
+ argumentArray[index + 1],
+ NumberStyles.None,
+ CultureInfo.InvariantCulture,
+ out var parsedProcessId))
+ {
+ processId = parsedProcessId;
+ index++;
+ continue;
+ }
+
+ remainingArguments.Add(argument);
+ }
+
+ return ([.. remainingArguments], processId);
+ }
+}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 7a7e7dff..34636209 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -2,6 +2,7 @@
using System.Reflection;
using System.Reflection.PortableExecutable;
using System.Runtime.Loader;
+using System.Text.Json;
using Weikio.PluginFramework.Abstractions;
using Weikio.PluginFramework.Catalogs;
using Weikio.PluginFramework.Context;
@@ -19,18 +20,37 @@ public sealed class NuGetPluginCatalog : IPluginCatalog
private readonly string sourceDir;
private readonly string tempDir;
+ private readonly int hostMajorVersion;
private readonly FolderPluginCatalogOptions options;
private CompositePluginCatalog innerCatalog = new();
public NuGetPluginCatalog(string sourceDir, FolderPluginCatalogOptions options)
- : this(sourceDir, DefaultTempDir, options)
+ : this(sourceDir, DefaultTempDir, AppInfo.Instance.Version.Major, options)
+ {
+ }
+
+ public NuGetPluginCatalog(
+ string sourceDir,
+ int hostMajorVersion,
+ FolderPluginCatalogOptions options)
+ : this(sourceDir, DefaultTempDir, hostMajorVersion, options)
{
}
internal NuGetPluginCatalog(string sourceDir, string tempDir, FolderPluginCatalogOptions options)
+ : this(sourceDir, tempDir, AppInfo.Instance.Version.Major, options)
+ {
+ }
+
+ internal NuGetPluginCatalog(
+ string sourceDir,
+ string tempDir,
+ int hostMajorVersion,
+ FolderPluginCatalogOptions options)
{
this.sourceDir = sourceDir;
this.tempDir = tempDir;
+ this.hostMajorVersion = hostMajorVersion;
this.options = options;
}
@@ -40,7 +60,10 @@ internal NuGetPluginCatalog(string sourceDir, string tempDir, FolderPluginCatalo
///
public async Task Initialize()
{
- SynchronizePluginFiles(this.sourceDir, this.tempDir);
+ var incompatiblePackages = GetIncompatiblePackageIds(
+ this.sourceDir,
+ this.hostMajorVersion);
+ SynchronizePluginFiles(this.sourceDir, this.tempDir, incompatiblePackages);
this.innerCatalog = CreateCatalog(this.tempDir, this.options);
await this.innerCatalog.Initialize().ConfigureAwait(false);
@@ -285,7 +308,10 @@ private static string GetHintKey(string fileName, bool isNative)
private static string GetSatelliteKey(string assemblyName, string cultureName)
=> $"{cultureName}:{assemblyName}";
- internal static void SynchronizePluginFiles(string source, string destination)
+ internal static void SynchronizePluginFiles(
+ string source,
+ string destination,
+ IReadOnlySet? excludedRootDirectories = null)
{
Directory.CreateDirectory(destination);
@@ -298,7 +324,8 @@ internal static void SynchronizePluginFiles(string source, string destination)
source,
isRoot: true,
sourceFiles,
- sourceDirectories);
+ sourceDirectories,
+ excludedRootDirectories);
}
foreach (var relativeDirectory in sourceDirectories.OrderBy(GetPathDepth))
@@ -363,12 +390,45 @@ private static bool IsManagementFile(string fileName)
=> fileName.Equals("nuget-manifest.json", StringComparison.OrdinalIgnoreCase)
|| fileName.StartsWith("nuget-manifest.json.tmp-", StringComparison.OrdinalIgnoreCase);
+ internal static IReadOnlySet GetIncompatiblePackageIds(
+ string sourceDirectory,
+ int hostMajorVersion)
+ {
+ var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
+ if (!File.Exists(manifestPath))
+ {
+ return new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+
+ try
+ {
+ using var stream = File.OpenRead(manifestPath);
+ var manifest = JsonSerializer.Deserialize(
+ stream,
+ NuGetPluginService.ManifestJsonOptions);
+ return manifest?.Packages
+ .Where(package => package.HostMajorVersion is not null
+ && package.HostMajorVersion != hostMajorVersion)
+ .Select(package => package.Id)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase)
+ ?? new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+ catch (Exception ex) when (ex is IOException
+ or UnauthorizedAccessException
+ or JsonException)
+ {
+ // 壊れたマニフェストはNuGetPluginService側で報告する。ここでは既存動作を維持する。
+ return new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+ }
+
private static void CollectSourceEntries(
string sourceRoot,
string currentDirectory,
bool isRoot,
Dictionary sourceFiles,
- HashSet sourceDirectories)
+ HashSet sourceDirectories,
+ IReadOnlySet? excludedRootDirectories)
{
foreach (var file in Directory.EnumerateFiles(currentDirectory))
{
@@ -382,7 +442,10 @@ private static void CollectSourceEntries(
foreach (var subDirectory in Directory.EnumerateDirectories(currentDirectory))
{
- if (isRoot && IsWorkingDirectory(Path.GetFileName(subDirectory)))
+ var directoryName = Path.GetFileName(subDirectory);
+ if (isRoot
+ && (IsWorkingDirectory(directoryName)
+ || excludedRootDirectories?.Contains(directoryName) is true))
{
continue;
}
@@ -394,7 +457,8 @@ private static void CollectSourceEntries(
subDirectory,
isRoot: false,
sourceFiles,
- sourceDirectories);
+ sourceDirectories,
+ excludedRootDirectories);
}
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 346f3087..c3aa7de6 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -4,6 +4,7 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NuGet.Packaging;
using NuGet.Versioning;
@@ -13,14 +14,15 @@ namespace WindowTranslator.Modules.PluginStore;
///
/// NuGet V3 REST APIを使用してプラグインパッケージの検索・インストール・管理を行うサービスです。
///
-public sealed class NuGetPluginService : IDisposable
+public sealed class NuGetPluginService : BackgroundService
{
private const string NuGetServiceIndexUrl = "https://api.nuget.org/v3/index.json";
internal const string PluginTag = "windowtranslator-plugin";
internal const string AbstractionsPackageId = "WindowTranslator.Abstractions";
private const int MaxConcurrentMetadataRequests = 8;
+ private static readonly TimeSpan PackageInformationRefreshInterval = TimeSpan.FromHours(1);
- private static readonly JsonSerializerOptions JsonOptions = new()
+ internal static readonly JsonSerializerOptions ManifestJsonOptions = new()
{
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
@@ -35,9 +37,16 @@ public sealed class NuGetPluginService : IDisposable
private readonly bool ownsHttpClient;
private readonly IReadOnlyDictionary hostPackageVersions;
private readonly INuGetPluginMetadataSource metadataSource;
+ private readonly App? app;
+ private readonly int hostMajorVersion;
private readonly SemaphoreSlim operationLock = new(1, 1);
+ private readonly SemaphoreSlim refreshLock = new(1, 1);
+ private readonly object snapshotLock = new();
+ private PluginStoreSnapshot packageSnapshot = PluginStoreSnapshot.Empty;
+ private long installedPackagesGeneration;
+ private int disposeState;
- public NuGetPluginService(ILogger logger)
+ public NuGetPluginService(ILogger logger, App app)
: this(
logger,
new HttpClient(new HttpClientHandler
@@ -48,7 +57,8 @@ public NuGetPluginService(ILogger logger)
Timeout = TimeSpan.FromSeconds(30),
},
Path.Combine(PathUtility.UserDir, "plugins"),
- ownsHttpClient: true)
+ ownsHttpClient: true,
+ app: app)
{
}
@@ -58,7 +68,9 @@ internal NuGetPluginService(
string userPluginsDir,
bool ownsHttpClient = false,
IReadOnlyDictionary? hostPackageVersions = null,
- INuGetPluginMetadataSource? metadataSource = null)
+ INuGetPluginMetadataSource? metadataSource = null,
+ App? app = null,
+ int? hostMajorVersion = null)
{
this.logger = logger;
this.httpClient = httpClient;
@@ -68,6 +80,85 @@ internal NuGetPluginService(
this.hostPackageVersions = hostPackageVersions ?? CreateHostPackageVersions();
this.metadataSource = metadataSource
?? new NuGetProtocolPluginMetadataSource(NuGetServiceIndexUrl);
+ this.app = app;
+ this.hostMajorVersion = hostMajorVersion ?? AppInfo.Instance.Version.Major;
+ }
+
+ internal event EventHandler? PackageInformationUpdated;
+
+ internal PluginStoreSnapshot PackageSnapshot
+ {
+ get
+ {
+ lock (this.snapshotLock)
+ {
+ return this.packageSnapshot;
+ }
+ }
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ if (this.app is not null)
+ {
+ await this.app.WaitForStartupAsync().ConfigureAwait(false);
+ }
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ await RefreshPackageInformationAsync(stoppingToken).ConfigureAwait(false);
+ await Task.Delay(PackageInformationRefreshInterval, stoppingToken).ConfigureAwait(false);
+ }
+ }
+
+ internal async Task RefreshPackageInformationAsync(CancellationToken cancellationToken = default)
+ {
+ await this.refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ var previousSnapshot = this.PackageSnapshot;
+ var packages = previousSnapshot.Packages;
+ Exception? error = null;
+ try
+ {
+ packages = await SearchPackagesAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ this.logger.LogWarning(ex, "NuGetからプラグイン情報を更新できませんでした。");
+ error = ex;
+ }
+
+ var installedGenerationBefore = Volatile.Read(ref this.installedPackagesGeneration);
+ IReadOnlyList installedPackages;
+ try
+ {
+ installedPackages = await GetInstalledPackagesAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ this.logger.LogWarning(ex, "インストール済みプラグイン情報を更新できませんでした。");
+ installedPackages = this.PackageSnapshot.InstalledPackages;
+ error = ex;
+ }
+
+ var installedGenerationAfter = Volatile.Read(ref this.installedPackagesGeneration);
+ if (installedGenerationBefore != installedGenerationAfter)
+ {
+ installedPackages = this.PackageSnapshot.InstalledPackages;
+ }
+ SetPackageSnapshot(
+ new(
+ IsInitialized: true,
+ InstalledPackages: installedPackages,
+ Packages: packages,
+ Error: error),
+ installedGenerationAfter);
+ }
+ finally
+ {
+ this.refreshLock.Release();
+ }
}
///
@@ -188,6 +279,7 @@ await installer.InstallAsync(
Directory.Move(stagingDir, targetDir);
stagingMoved = true;
await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
+ UpdateInstalledPackages(updatedManifest.Packages);
try
{
@@ -264,6 +356,7 @@ public async Task UninstallPackageAsync(string packageId, CancellationToken canc
try
{
await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
+ UpdateInstalledPackages(updatedManifest.Packages);
}
catch
{
@@ -316,8 +409,28 @@ public async Task UninstallPackageAsync(string packageId, CancellationToken canc
///
public async Task> GetInstalledPackagesAsync(CancellationToken cancellationToken = default)
{
- var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
- return manifest.Packages;
+ await this.operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ try
+ {
+ var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ var migratedPackages = manifest.Packages
+ .Select(package => package.HostMajorVersion is null
+ ? package with { HostMajorVersion = this.hostMajorVersion }
+ : package)
+ .ToList();
+ if (migratedPackages.Where((package, index) =>
+ package != manifest.Packages[index]).Any())
+ {
+ manifest = new InstalledManifest(migratedPackages);
+ await SaveManifestAsync(manifest, cancellationToken).ConfigureAwait(false);
+ }
+
+ return GetCompatibilityAwarePackages(manifest.Packages);
+ }
+ finally
+ {
+ this.operationLock.Release();
+ }
}
private async Task CreateCompatiblePackageInfoAsync(
@@ -373,14 +486,17 @@ private bool HasCompatibleAbstractionsDependency(
return dependency.VersionRange?.Satisfies(hostVersion) is not false;
}
- private static InstalledManifest AddOrUpdatePackage(
+ private InstalledManifest AddOrUpdatePackage(
InstalledManifest manifest,
string packageId,
string version)
{
var packages = manifest.Packages.ToList();
var existing = packages.FindIndex(p => p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase));
- var newEntry = new InstalledPackageInfo(packageId, version);
+ var newEntry = new InstalledPackageInfo(
+ packageId,
+ version,
+ this.hostMajorVersion);
if (existing >= 0)
{
packages[existing] = newEntry;
@@ -421,6 +537,65 @@ private static Dictionary CreateHostPackageVersions()
};
}
+ private InstalledPackageInfo[] GetCompatibilityAwarePackages(
+ IEnumerable packages)
+ => packages
+ .Select(package => package with
+ {
+ IsCompatible = package.HostMajorVersion is null
+ || package.HostMajorVersion == this.hostMajorVersion,
+ })
+ .ToArray();
+
+ private void UpdateInstalledPackages(IEnumerable packages)
+ {
+ lock (this.snapshotLock)
+ {
+ this.installedPackagesGeneration++;
+ this.packageSnapshot = this.packageSnapshot with
+ {
+ InstalledPackages = GetCompatibilityAwarePackages(packages),
+ };
+ }
+ NotifyPackageInformationUpdated();
+ }
+
+ private void SetPackageSnapshot(
+ PluginStoreSnapshot snapshot,
+ long? expectedInstalledPackagesGeneration = null)
+ {
+ lock (this.snapshotLock)
+ {
+ if (expectedInstalledPackagesGeneration is not null
+ && expectedInstalledPackagesGeneration != this.installedPackagesGeneration)
+ {
+ snapshot = snapshot with
+ {
+ InstalledPackages = this.packageSnapshot.InstalledPackages,
+ };
+ }
+ this.packageSnapshot = snapshot;
+ }
+
+ NotifyPackageInformationUpdated();
+ }
+
+ private void NotifyPackageInformationUpdated()
+ {
+ foreach (EventHandler handler in this.PackageInformationUpdated?.GetInvocationList()
+ .Cast() ?? [])
+ {
+ try
+ {
+ handler(this, EventArgs.Empty);
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogWarning(ex, "プラグイン情報更新イベントの通知に失敗しました。");
+ }
+ }
+ }
+
private static InstalledManifest RemovePackage(InstalledManifest manifest, string packageId)
=> new([.. manifest.Packages.Where(p =>
!p.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase))]);
@@ -437,7 +612,7 @@ private async Task LoadManifestAsync(CancellationToken cancel
await using var fs = File.OpenRead(this.manifestPath);
var manifest = await JsonSerializer.DeserializeAsync(
fs,
- JsonOptions,
+ ManifestJsonOptions,
cancellationToken).ConfigureAwait(false)
?? throw new InvalidDataException("プラグインマニフェストが空です。");
if (manifest.Packages is null)
@@ -469,7 +644,11 @@ private async Task SaveManifestAsync(InstalledManifest manifest, CancellationTok
bufferSize: 4096,
useAsync: true))
{
- await JsonSerializer.SerializeAsync(fs, manifest, JsonOptions, cancellationToken).ConfigureAwait(false);
+ await JsonSerializer.SerializeAsync(
+ fs,
+ manifest,
+ ManifestJsonOptions,
+ cancellationToken).ConfigureAwait(false);
await fs.FlushAsync(cancellationToken).ConfigureAwait(false);
}
@@ -537,13 +716,20 @@ private static void TryDeleteFile(string path)
}
}
- public void Dispose()
+ public override void Dispose()
{
+ if (Interlocked.Exchange(ref this.disposeState, 1) != 0)
+ {
+ return;
+ }
+
+ base.Dispose();
if (this.ownsHttpClient)
{
this.httpClient.Dispose();
}
this.operationLock.Dispose();
+ this.refreshLock.Dispose();
}
}
@@ -562,8 +748,21 @@ public record NuGetPackageInfo(
/// インストール済みパッケージ情報
public record InstalledPackageInfo(
string Id,
- string Version
-);
+ string Version,
+ int? HostMajorVersion = null)
+{
+ [JsonIgnore]
+ public bool IsCompatible { get; init; } = true;
+}
/// NuGetプラグインの管理マニフェスト
public record InstalledManifest(List Packages);
+
+internal sealed record PluginStoreSnapshot(
+ bool IsInitialized,
+ IReadOnlyList InstalledPackages,
+ IReadOnlyList Packages,
+ Exception? Error)
+{
+ public static PluginStoreSnapshot Empty { get; } = new(false, [], [], null);
+}
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
index 8e50a79e..4f9e77fb 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
@@ -148,7 +148,7 @@
Icon="{ui:SymbolIcon ArrowSync24}"
IsEnabled="{Binding CanInstall}"
Style="{StaticResource InstallButtonStyle}"
- Visibility="{Binding IsUpdateAvailable, Converter={StaticResource b2vConv}}" />
+ Visibility="{Binding CanUpdate, Converter={StaticResource b2vConv}}" />
/// プラグインストアのViewModel
///
-public partial class PluginStoreViewModel : ObservableObject
+public partial class PluginStoreViewModel : ObservableObject, IDisposable
{
private readonly NuGetPluginService nugetService;
private readonly ILogger logger;
private readonly IContentDialogService dialogService;
private CancellationTokenSource? readmeLoadCancellation;
private PluginPackageViewModel? selectedPackage;
+ private PluginStoreSnapshot? pendingSnapshot;
+ private PluginStoreSnapshot? appliedSnapshot;
+ private bool disposed;
[ObservableProperty]
private bool isLoading;
@@ -66,6 +70,7 @@ public PluginStoreViewModel(
this.nugetService = nugetService;
this.logger = logger;
this.dialogService = dialogService;
+ this.nugetService.PackageInformationUpdated += OnPackageInformationUpdated;
}
///
@@ -82,41 +87,14 @@ public async Task LoadAsync(CancellationToken cancellationToken = default)
try
{
- var installed = await this.nugetService.GetInstalledPackagesAsync(cancellationToken).ConfigureAwait(true);
- var installedDict = installed.ToDictionary(p => p.Id, StringComparer.OrdinalIgnoreCase);
-
- this.Packages.Clear();
- foreach (var inst in installed)
- {
- this.Packages.Add(new PluginPackageViewModel(
- new NuGetPackageInfo(inst.Id, inst.Version, inst.Id, string.Empty, string.Empty, null, null),
- isInstalled: true,
- installedVersion: inst.Version));
- }
-
- var packages = await this.nugetService.SearchPackagesAsync(cancellationToken).ConfigureAwait(true);
- this.logger.LogInformation("NuGetから{Count}件のプラグインパッケージを取得しました。", packages.Count);
-
- this.Packages.Clear();
- foreach (var pkg in packages)
+ if (!this.nugetService.PackageSnapshot.IsInitialized)
{
- installedDict.TryGetValue(pkg.Id, out var installedInfo);
- var isInstalled = installedInfo is not null;
- var installedVersion = installedInfo?.Version;
- this.Packages.Add(new PluginPackageViewModel(pkg, isInstalled, installedVersion));
+ await this.nugetService
+ .RefreshPackageInformationAsync(cancellationToken)
+ .ConfigureAwait(true);
}
- // インストール済みだがNuGetに見つからないパッケージも表示
- foreach (var inst in installed)
- {
- if (!this.Packages.Any(p => p.Id.Equals(inst.Id, StringComparison.OrdinalIgnoreCase)))
- {
- this.Packages.Add(new PluginPackageViewModel(
- new NuGetPackageInfo(inst.Id, inst.Version, inst.Id, string.Empty, string.Empty, null, null),
- isInstalled: true,
- installedVersion: inst.Version));
- }
- }
+ ApplyPackageSnapshot(this.nugetService.PackageSnapshot);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
@@ -133,6 +111,114 @@ public async Task LoadAsync(CancellationToken cancellationToken = default)
}
}
+ private void OnPackageInformationUpdated(object? sender, EventArgs e)
+ {
+ if (this.disposed)
+ {
+ return;
+ }
+
+ var snapshot = this.nugetService.PackageSnapshot;
+ var dispatcher = Application.Current?.Dispatcher;
+ if (dispatcher is null || dispatcher.CheckAccess())
+ {
+ ApplyPackageSnapshot(snapshot);
+ }
+ else
+ {
+ _ = dispatcher.BeginInvoke(() => ApplyPackageSnapshot(snapshot));
+ }
+ }
+
+ private void ApplyPackageSnapshot(PluginStoreSnapshot snapshot)
+ {
+ if (this.disposed)
+ {
+ return;
+ }
+ if (ReferenceEquals(this.appliedSnapshot, snapshot))
+ {
+ return;
+ }
+ if (this.Packages.Any(package => package.IsInstalling))
+ {
+ this.pendingSnapshot = snapshot;
+ return;
+ }
+
+ this.pendingSnapshot = null;
+ this.appliedSnapshot = snapshot;
+ var selectedPackageId = this.SelectedPackage?.Id;
+ var prereleaseSelections = this.Packages.ToDictionary(
+ package => package.Id,
+ package => package.UsePrerelease,
+ StringComparer.OrdinalIgnoreCase);
+ var installedPackages = snapshot.InstalledPackages.ToDictionary(
+ package => package.Id,
+ StringComparer.OrdinalIgnoreCase);
+
+ this.Packages.Clear();
+ foreach (var packageInfo in snapshot.Packages)
+ {
+ installedPackages.TryGetValue(packageInfo.Id, out var installedPackage);
+ var package = new PluginPackageViewModel(
+ packageInfo,
+ isInstalled: installedPackage is not null,
+ installedVersion: installedPackage?.Version,
+ isCompatible: installedPackage?.IsCompatible ?? true,
+ hasCompatiblePackageVersion: true);
+ if (package.HasPrereleaseVersion
+ && prereleaseSelections.TryGetValue(package.Id, out var usePrerelease))
+ {
+ package.UsePrerelease = usePrerelease;
+ }
+ this.Packages.Add(package);
+ }
+
+ foreach (var installedPackage in snapshot.InstalledPackages)
+ {
+ if (this.Packages.Any(package => package.Id.Equals(
+ installedPackage.Id,
+ StringComparison.OrdinalIgnoreCase)))
+ {
+ continue;
+ }
+
+ this.Packages.Add(new PluginPackageViewModel(
+ new NuGetPackageInfo(
+ installedPackage.Id,
+ installedPackage.Version,
+ installedPackage.Id,
+ string.Empty,
+ string.Empty,
+ null,
+ null),
+ isInstalled: true,
+ installedVersion: installedPackage.Version,
+ isCompatible: installedPackage.IsCompatible,
+ hasCompatiblePackageVersion: false));
+ }
+
+ this.SelectedPackage = selectedPackageId is null
+ ? null
+ : this.Packages.FirstOrDefault(package => package.Id.Equals(
+ selectedPackageId,
+ StringComparison.OrdinalIgnoreCase));
+ this.ErrorMessage = snapshot.Error is null ? null : Resources.NuGetSearchFailed;
+ this.logger.LogInformation(
+ "バックグラウンド更新から{Count}件のプラグインパッケージを反映しました。",
+ snapshot.Packages.Count);
+ }
+
+ private void ApplyPendingSnapshot()
+ {
+ if (this.pendingSnapshot is { } snapshot
+ && !this.Packages.Any(package => package.IsInstalling))
+ {
+ ApplyPackageSnapshot(snapshot);
+ }
+ }
+
///
/// プラグインをインストールまたは更新します。
///
@@ -160,17 +246,14 @@ await this.nugetService.InstallPackageAsync(
package.IsInstalled = true;
package.InstalledVersion = version;
+ package.IsCompatible = true;
package.InstallProgress = 0;
this.logger.LogInformation("プラグインのインストール完了: {PackageId}", package.Id);
- // 再起動が必要な旨を表示
- await this.dialogService.ShowSimpleDialogAsync(new()
- {
- Title = Resources.PluginInstallSuccess,
- Content = Resources.RestartRequired,
- CloseButtonText = Resources.Close,
- }, cancellationToken).ConfigureAwait(true);
+ await ShowRestartDialogAsync(
+ Resources.PluginInstallSuccess,
+ cancellationToken).ConfigureAwait(true);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
@@ -188,6 +271,7 @@ await this.dialogService.ShowAlertAsync(
finally
{
package.IsInstalling = false;
+ ApplyPendingSnapshot();
}
}
@@ -208,18 +292,15 @@ public async Task UninstallAsync(PluginPackageViewModel package)
if (result != Wpf.Ui.Controls.ContentDialogResult.Primary)
return;
+ package.IsInstalling = true;
try
{
await this.nugetService.UninstallPackageAsync(package.Id).ConfigureAwait(true);
package.IsInstalled = false;
package.InstalledVersion = null;
+ package.IsCompatible = true;
- await this.dialogService.ShowSimpleDialogAsync(new()
- {
- Title = Resources.Uninstall,
- Content = Resources.RestartRequired,
- CloseButtonText = Resources.Close,
- }).ConfigureAwait(true);
+ await ShowRestartDialogAsync(Resources.Uninstall).ConfigureAwait(true);
}
catch (Exception ex)
{
@@ -229,6 +310,28 @@ await this.dialogService.ShowAlertAsync(
ex.Message,
Resources.Close).ConfigureAwait(true);
}
+ finally
+ {
+ package.IsInstalling = false;
+ ApplyPendingSnapshot();
+ }
+ }
+
+ private async Task ShowRestartDialogAsync(
+ string title,
+ CancellationToken cancellationToken = default)
+ {
+ var result = await this.dialogService.ShowSimpleDialogAsync(new()
+ {
+ Title = title,
+ Content = Resources.RestartRequired,
+ PrimaryButtonText = Resources.RestartNow,
+ CloseButtonText = Resources.Close,
+ }, cancellationToken).ConfigureAwait(true);
+ if (result == Wpf.Ui.Controls.ContentDialogResult.Primary)
+ {
+ ApplicationRestart.Restart();
+ }
}
private void OnSelectedPackagePropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -301,6 +404,20 @@ private async Task LoadPackageReadmeAsync(
}
}
+ public void Dispose()
+ {
+ if (this.disposed)
+ {
+ return;
+ }
+
+ this.disposed = true;
+ this.nugetService.PackageInformationUpdated -= OnPackageInformationUpdated;
+ this.readmeLoadCancellation?.Cancel();
+ this.readmeLoadCancellation = null;
+ GC.SuppressFinalize(this);
+ }
+
}
///
@@ -319,10 +436,18 @@ public partial class PluginPackageViewModel : ObservableObject
: this.ReleaseVersion;
public bool HasPrereleaseVersion => this.PrereleaseVersion is not null;
public bool CanInstall => !this.IsInstalling && this.LatestVersion is not null;
+ public bool RequiresReinstall => this.IsInstalled
+ && !this.IsCompatible
+ && this.hasCompatiblePackageVersion
+ && this.LatestVersion is not null;
+ public bool CanUpdate => this.IsUpdateAvailable || this.RequiresReinstall;
public string? ProjectUrl { get; }
public string? LicenseUrl { get; }
+ private readonly bool hasCompatiblePackageVersion;
[ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(RequiresReinstall))]
+ [NotifyPropertyChangedFor(nameof(CanUpdate))]
private bool isInstalled;
[ObservableProperty]
@@ -331,8 +456,15 @@ public partial class PluginPackageViewModel : ObservableObject
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(StatusText))]
+ [NotifyPropertyChangedFor(nameof(CanUpdate))]
private bool isUpdateAvailable;
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(StatusText))]
+ [NotifyPropertyChangedFor(nameof(RequiresReinstall))]
+ [NotifyPropertyChangedFor(nameof(CanUpdate))]
+ private bool isCompatible;
+
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(CanInstall))]
private bool isInstalling;
@@ -351,6 +483,8 @@ public partial class PluginPackageViewModel : ObservableObject
[NotifyPropertyChangedFor(nameof(LatestVersion))]
[NotifyPropertyChangedFor(nameof(CanInstall))]
[NotifyPropertyChangedFor(nameof(StatusText))]
+ [NotifyPropertyChangedFor(nameof(RequiresReinstall))]
+ [NotifyPropertyChangedFor(nameof(CanUpdate))]
private bool usePrerelease;
public bool HasReadme => !string.IsNullOrWhiteSpace(this.ReadmeMarkdown);
@@ -359,6 +493,8 @@ public string StatusText
{
get
{
+ if (this.IsInstalled && !this.IsCompatible)
+ return Resources.PluginIncompatible;
if (this.IsUpdateAvailable
&& this.InstalledVersion is not null
&& this.LatestVersion is not null)
@@ -372,7 +508,9 @@ public string StatusText
public PluginPackageViewModel(
NuGetPackageInfo info,
bool isInstalled,
- string? installedVersion)
+ string? installedVersion,
+ bool isCompatible = true,
+ bool hasCompatiblePackageVersion = true)
{
var versions = new[] { info.Version }
.Concat(info.Versions ?? [])
@@ -398,8 +536,10 @@ public PluginPackageViewModel(
.FirstOrDefault();
this.ProjectUrl = info.ProjectUrl;
this.LicenseUrl = info.LicenseUrl;
+ this.hasCompatiblePackageVersion = hasCompatiblePackageVersion;
this.isInstalled = isInstalled;
this.installedVersion = installedVersion;
+ this.isCompatible = isCompatible;
this.usePrerelease = this.PrereleaseVersion is not null
&& NuGetVersion.TryParse(installedVersion, out var installed)
&& installed.IsPrerelease;
@@ -412,6 +552,8 @@ public PluginPackageViewModel(
partial void OnUsePrereleaseChanged(bool value) => RefreshUpdateAvailable();
+ partial void OnIsCompatibleChanged(bool value) => RefreshUpdateAvailable();
+
private void RefreshUpdateAvailable()
{
this.IsUpdateAvailable = this.IsInstalled
diff --git a/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs b/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
index 5e15a387..f0e21189 100644
--- a/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
+++ b/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
@@ -338,6 +338,7 @@ private DisposeAction EnterBusy()
public void Dispose()
{
this.updateChecker.UpdateAvailable -= UpdateChecker_UpdateAvailable;
+ this.PluginStore.Dispose();
}
}
diff --git a/WindowTranslator/Program.cs b/WindowTranslator/Program.cs
index db70867c..be8d99ef 100644
--- a/WindowTranslator/Program.cs
+++ b/WindowTranslator/Program.cs
@@ -38,6 +38,8 @@
//Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo("it");
//Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("it");
+args = ApplicationRestart.WaitForPreviousProcess(args);
+
#if NO_MUTEX
var createdNew = true;
#else
@@ -119,6 +121,7 @@
var userPluginsDir = Path.Combine(PathUtility.UserDir, "plugins");
pluginFolderCatalog.AddCatalog(new NuGetPluginCatalog(
userPluginsDir,
+ AppInfo.Instance.Version.Major,
new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } }));
builder.Services.AddPluginCatalog(pluginFolderCatalog);
@@ -158,7 +161,8 @@
builder.Services.AddPresentation();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
-builder.Services.AddSingleton();
+builder.Services.AddSingleton()
+ .AddHostedService(sp => sp.GetRequiredService());
builder.Services.AddTransient();
builder.Services.AddTransient, ConfigureUserSettings>();
builder.Services.Configure(builder.Configuration.GetSection(nameof(UserSettings.Common)));
diff --git a/WindowTranslator/Properties/Resources.Designer.cs b/WindowTranslator/Properties/Resources.Designer.cs
index b4e15bb9..a526e1fb 100644
--- a/WindowTranslator/Properties/Resources.Designer.cs
+++ b/WindowTranslator/Properties/Resources.Designer.cs
@@ -457,6 +457,11 @@ internal Resources() {
///
public static string PluginInstallSuccess => ResourceManager.GetString("PluginInstallSuccess", resourceCulture) ?? string.Empty;
+ ///
+ /// "現在のWindowTranslatorメジャーバージョンとは互換性がありません。" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string PluginIncompatible => ResourceManager.GetString("PluginIncompatible", resourceCulture) ?? string.Empty;
+
///
/// "プラグイン" に類似しているローカライズされた文字列を検索します。
///
@@ -482,6 +487,11 @@ internal Resources() {
///
public static string RegisterAutoStart => ResourceManager.GetString("RegisterAutoStart", resourceCulture) ?? string.Empty;
+ ///
+ /// "今すぐ再起動" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string RestartNow => ResourceManager.GetString("RestartNow", resourceCulture) ?? string.Empty;
+
///
/// "プラグインの変更を適用するには、WindowTranslatorを再起動してください。" に類似しているローカライズされた文字列を検索します。
///
diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx
index 23eed21d..21dfeaaa 100644
--- a/WindowTranslator/Properties/Resources.en.resx
+++ b/WindowTranslator/Properties/Resources.en.resx
@@ -504,6 +504,12 @@ Monitors are not supported.
Installation failed
+
+ This plugin is incompatible with the current WindowTranslator major version.
+
+
+ Restart now
+
Please restart WindowTranslator to apply plugin changes.
diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx
index e9e8b24f..0774e624 100644
--- a/WindowTranslator/Properties/Resources.resx
+++ b/WindowTranslator/Properties/Resources.resx
@@ -504,6 +504,12 @@
インストール失敗
+
+ 現在のWindowTranslatorメジャーバージョンとは互換性がありません。
+
+
+ 今すぐ再起動
+
プラグインの変更を適用するには、WindowTranslatorを再起動してください。
diff --git a/docs/plugin.md b/docs/plugin.md
index 942a4f6f..7b59fcf3 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -127,7 +127,7 @@ public class MyTranslateModule : ITranslateModule { ... }
2. 「プラグイン」タブを選択します。
3. 利用するプラグインの「インストール」を選択します。
プレリリース版を利用する場合は、そのプラグインの「プレリリース」にチェックを入れます。
-4. インストール完了後に WindowTranslator を再起動します。
+4. インストール完了後のダイアログから WindowTranslator を再起動します。
NuGetパッケージで宣言されたランタイム依存関係も再帰的に取得されます。
同じ依存パッケージに両立しないバージョン条件がある場合は、既存の
@@ -137,6 +137,11 @@ NuGetパッケージで宣言されたランタイム依存関係も再帰的に
自動インストールされることはありません。インストールはプラグインストアで
利用者が明示的に実行した場合だけ行われます。
+プラグイン情報は WindowTranslator の起動後にバックグラウンドで更新されます。
+プラグインをインストールした WindowTranslator と現在のメジャーバージョンが異なる場合、
+そのプラグインは互換性がないものとして起動時のロード対象から除外されます。
+互換バージョンを再インストールすると、次回起動から再び利用できます。
+
アンインストールすると管理フォルダのパッケージは直ちに削除されます。
実行中に読み込まれたプラグインを停止するには、WindowTranslator の再起動が必要です。
From 38a25ccdb115228c8ec498908a947623301f6fd6 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Tue, 4 Aug 2026 09:46:50 +0900
Subject: [PATCH 14/43] =?UTF-8?q?NuGet=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E9=80=9A=E4=BF=A1=E3=82=92SDK=E3=81=B8=E7=B5=B1?=
=?UTF-8?q?=E5=90=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Directory.Packages.props | 1 +
.../NuGetPluginServiceTests.cs | 516 +++++++++++-------
.../PluginStore/NuGetPackageInstaller.cs | 83 ++-
.../Modules/PluginStore/NuGetPluginCatalog.cs | 5 +-
.../Modules/PluginStore/NuGetPluginService.cs | 381 ++++++-------
.../NuGetProtocolPluginMetadataSource.cs | 122 -----
.../PluginStore/PluginCompatibility.cs | 21 +
WindowTranslator/Program.cs | 20 +-
WindowTranslator/WindowTranslator.csproj | 3 +-
9 files changed, 582 insertions(+), 570 deletions(-)
delete mode 100644 WindowTranslator/Modules/PluginStore/NuGetProtocolPluginMetadataSource.cs
create mode 100644 WindowTranslator/Modules/PluginStore/PluginCompatibility.cs
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 26c77095..91bbe10e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -23,6 +23,7 @@
+
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index 9744f4ad..5f1664bf 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -11,9 +11,12 @@
using System.Xml.Linq;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging.Abstractions;
+using NuGet.Configuration;
using NuGet.Frameworks;
using NuGet.Packaging;
using NuGet.Packaging.Core;
+using NuGet.Protocol;
+using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
using Weikio.PluginFramework.Catalogs;
using Weikio.PluginFramework.Context;
@@ -73,8 +76,7 @@ public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories
["lib/netstandard2.0/Transitive.Package.dll"] = "transitive"u8.ToArray(),
}));
- using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory, hostMajorVersion: 7);
+ using var service = CreateService(handler, testDirectory, hostMajorVersion: 7);
await service.InstallPackageAsync("Root.Plugin", "1.0.0");
@@ -152,8 +154,7 @@ public async Task ManifestWriteFailureRestoresThePreviousPluginDirectory()
["lib/net10.0/Root.Plugin.dll"] = "version-two"u8.ToArray(),
}));
- using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory);
+ using var service = CreateService(handler, testDirectory);
await service.InstallPackageAsync("Root.Plugin", "1.0.0");
var manifestPath = Path.Combine(testDirectory, "nuget-manifest.json");
@@ -213,8 +214,7 @@ public async Task UninstallRemovesManagedFilesImmediatelyAndAllowsManualReinstal
["lib/net10.0/Root.Plugin.dll"] = "version-two"u8.ToArray(),
}));
- using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory);
+ using var service = CreateService(handler, testDirectory);
await service.InstallPackageAsync("Root.Plugin", "1.0.0");
await service.UninstallPackageAsync("Root.Plugin");
@@ -251,8 +251,7 @@ public async Task UninstallRestoresManagedFilesWhenManifestUpdateFails()
["lib/net10.0/Root.Plugin.dll"] = "version-one"u8.ToArray(),
}));
- using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory);
+ using var service = CreateService(handler, testDirectory);
await service.InstallPackageAsync("Root.Plugin", "1.0.0");
var manifestPath = Path.Combine(testDirectory, "nuget-manifest.json");
@@ -313,8 +312,7 @@ public async Task DependencyWithIncompatibleLibStillInstallsCompatibleNativeAsse
[$"runtimes/{RuntimeIdentifier}/native/compatible.dll"] = "native"u8.ToArray(),
}));
- using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory);
+ using var service = CreateService(handler, testDirectory);
await service.InstallPackageAsync("Root.Plugin", "1.0.0");
@@ -364,8 +362,7 @@ await File.WriteAllTextAsync(
["lib/net10.0/Root.Plugin.dll"] = "replacement"u8.ToArray(),
}));
- using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory);
+ using var service = CreateService(handler, testDirectory);
await Assert.ThrowsAsync(
() => service.InstallPackageAsync("Root.Plugin", "2.0.0"));
@@ -402,8 +399,7 @@ await File.WriteAllTextAsync(
}));
using var handler = new InMemoryNuGetHandler();
- using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory);
+ using var service = CreateService(handler, testDirectory);
var installed = Assert.Single(await service.GetInstalledPackagesAsync());
@@ -427,9 +423,8 @@ await File.WriteAllTextAsync(
JsonSerializer.Serialize(new InstalledManifest(
[new InstalledPackageInfo("Legacy.Plugin", "1.0.0")])));
using var handler = new InMemoryNuGetHandler();
- using var client = new HttpClient(handler);
using var service = CreateService(
- client,
+ handler,
testDirectory,
hostMajorVersion: 7);
@@ -453,7 +448,7 @@ await File.WriteAllTextAsync(
}
[Fact]
- public async Task InstalledPackageFromAnotherHostMajorVersionIsMarkedIncompatible()
+ public async Task InstalledPackageCompatibilityFollowsValidationSetting()
{
var testDirectory = CreateTestDirectory();
try
@@ -463,15 +458,14 @@ await File.WriteAllTextAsync(
JsonSerializer.Serialize(new InstalledManifest(
[new InstalledPackageInfo("Old.Plugin", "1.0.0", HostMajorVersion: 6)])));
using var handler = new InMemoryNuGetHandler();
- using var client = new HttpClient(handler);
using var service = CreateService(
- client,
+ handler,
testDirectory,
hostMajorVersion: 7);
var package = Assert.Single(await service.GetInstalledPackagesAsync());
- Assert.False(package.IsCompatible);
+ Assert.Equal(PluginCompatibility.ValidationDisabled, package.IsCompatible);
Assert.Equal(6, package.HostMajorVersion);
}
finally
@@ -487,27 +481,22 @@ public async Task BackgroundServiceRefreshesPluginInformationWithoutOpeningSetti
try
{
using var handler = new InMemoryNuGetHandler();
- var metadataSource = new InMemoryNuGetMetadataSource
- {
- SearchResults =
+ handler.SearchResults =
[
- new NuGetPluginSearchMetadata(
+ CreatePackageSearchMetadata(
"Background.Plugin",
"Background Plugin",
null,
null,
null,
null),
- ],
- };
- metadataSource.AddVersions(
+ ];
+ handler.AddMetadataVersions(
"Background.Plugin",
CreatePluginVersionMetadata("1.0.0"));
- using var client = new HttpClient(handler);
using var service = CreateService(
- client,
- testDirectory,
- metadataSource: metadataSource);
+ handler,
+ testDirectory);
var updated = new TaskCompletionSource(
TaskCreationOptions.RunContinuationsAsynchronously);
service.PackageInformationUpdated += (_, _) => updated.TrySetResult();
@@ -528,7 +517,7 @@ public async Task BackgroundServiceRefreshesPluginInformationWithoutOpeningSetti
Assert.Equal(
"Background.Plugin",
Assert.Single(service.PackageSnapshot.Packages).Id);
- Assert.Equal(["windowtranslator-plugin"], metadataSource.RequestedTags);
+ Assert.Equal(["tags:windowtranslator-plugin"], handler.RequestedSearchTerms);
}
finally
{
@@ -548,15 +537,10 @@ await File.WriteAllTextAsync(
[new InstalledPackageInfo("Installed.Plugin", "1.2.3")])),
Encoding.UTF8);
using var handler = new InMemoryNuGetHandler();
- using var client = new HttpClient(handler);
- var metadataSource = new InMemoryNuGetMetadataSource
- {
- SearchException = new HttpRequestException("NuGet search failed."),
- };
+ handler.SearchException = new HttpRequestException("NuGet search failed.");
using var service = CreateService(
- client,
- testDirectory,
- metadataSource: metadataSource);
+ handler,
+ testDirectory);
var viewModel = new PluginStoreViewModel(
service,
NullLogger.Instance,
@@ -569,7 +553,7 @@ [new InstalledPackageInfo("Installed.Plugin", "1.2.3")])),
Assert.Equal("1.2.3", package.InstalledVersion);
Assert.True(package.IsInstalled);
Assert.NotNull(viewModel.ErrorMessage);
- Assert.Equal(["windowtranslator-plugin"], metadataSource.RequestedTags);
+ Assert.Equal(["tags:windowtranslator-plugin"], handler.RequestedSearchTerms);
}
finally
{
@@ -584,29 +568,24 @@ public async Task SearchReturnsReleaseAndPrereleaseVersions()
try
{
using var handler = new InMemoryNuGetHandler();
- var metadataSource = new InMemoryNuGetMetadataSource
- {
- SearchResults =
+ handler.SearchResults =
[
- new NuGetPluginSearchMetadata(
+ CreatePackageSearchMetadata(
"Test.Plugin",
"Test Plugin",
"Test description",
"WindowTranslator.Tests",
null,
null),
- ],
- };
- metadataSource.AddVersions(
+ ];
+ handler.AddMetadataVersions(
"Test.Plugin",
CreatePluginVersionMetadata("1.0.0"),
CreatePluginVersionMetadata("1.1.0-beta.1"),
CreatePluginVersionMetadata("1.1.0-beta.2"));
- using var client = new HttpClient(handler);
using var service = CreateService(
- client,
- testDirectory,
- metadataSource: metadataSource);
+ handler,
+ testDirectory);
var package = Assert.Single(await service.SearchPackagesAsync());
@@ -615,7 +594,7 @@ public async Task SearchReturnsReleaseAndPrereleaseVersions()
Assert.Equal(
["1.0.0", "1.1.0-beta.1", "1.1.0-beta.2"],
package.Versions);
- Assert.Equal([true], metadataSource.RequestedPrereleaseOptions);
+ Assert.Equal([true], handler.RequestedPrereleaseOptions);
}
finally
{
@@ -630,52 +609,51 @@ public async Task SearchKeepsOnlyVersionsWithCompatibleDirectAbstractionsDepende
try
{
using var handler = new InMemoryNuGetHandler();
- var metadataSource = new InMemoryNuGetMetadataSource
- {
- SearchResults =
+ handler.SearchResults =
[
- new NuGetPluginSearchMetadata(
+ CreatePackageSearchMetadata(
"Compatible.Plugin",
null,
null,
null,
null,
null),
- new NuGetPluginSearchMetadata(
+ CreatePackageSearchMetadata(
"Missing.Dependency.Plugin",
null,
null,
null,
null,
null),
- ],
- };
- metadataSource.AddVersions(
+ ];
+ handler.AddMetadataVersions(
"Compatible.Plugin",
CreatePluginVersionMetadata("1.0.0", "[1.0.0, 2.0.0)"),
CreatePluginVersionMetadata("2.0.0", "[2.0.0, 3.0.0)"));
- metadataSource.AddVersions(
+ handler.AddMetadataVersions(
"Missing.Dependency.Plugin",
CreatePluginVersionMetadata(
"1.0.0",
includeAbstractionsDependency: false));
- using var client = new HttpClient(handler);
using var service = CreateService(
- client,
+ handler,
testDirectory,
new Dictionary(StringComparer.OrdinalIgnoreCase)
{
["WindowTranslator.Abstractions"] = NuGetVersion.Parse("1.5.0"),
- },
- metadataSource);
+ });
var package = Assert.Single(await service.SearchPackagesAsync());
Assert.Equal("Compatible.Plugin", package.Id);
- Assert.Equal("1.0.0", package.Version);
- Assert.Equal(["1.0.0"], package.Versions);
- Assert.Equal(["windowtranslator-plugin"], metadataSource.RequestedTags);
+ Assert.Equal(
+ PluginCompatibility.ValidationDisabled ? "2.0.0" : "1.0.0",
+ package.Version);
+ Assert.Equal(
+ PluginCompatibility.ValidationDisabled ? ["1.0.0", "2.0.0"] : ["1.0.0"],
+ package.Versions);
+ Assert.Equal(["tags:windowtranslator-plugin"], handler.RequestedSearchTerms);
}
finally
{
@@ -775,7 +753,7 @@ public void RestartArgumentsAreNotForwardedToTheRestartedApplication()
}
[Fact]
- public async Task InstallRejectsPackageRequiringNewerHostAbstractions()
+ public async Task InstallCompatibilityFollowsValidationSetting()
{
var testDirectory = CreateTestDirectory();
try
@@ -798,22 +776,28 @@ public async Task InstallRejectsPackageRequiringNewerHostAbstractions()
["lib/net10.0/Root.Plugin.dll"] = "root"u8.ToArray(),
}));
- using var client = new HttpClient(handler);
using var service = CreateService(
- client,
+ handler,
testDirectory,
new Dictionary(StringComparer.OrdinalIgnoreCase)
{
["WindowTranslator.Abstractions"] = NuGetVersion.Parse("1.5.0"),
});
- var exception = await Assert.ThrowsAsync(
- () => service.InstallPackageAsync("Root.Plugin", "2.0.0"));
-
- Assert.Contains("WindowTranslator.Abstractions", exception.Message);
- Assert.Contains("[2.0.0, 3.0.0)", exception.Message);
- Assert.False(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
- Assert.Empty(await service.GetInstalledPackagesAsync());
+ if (PluginCompatibility.ValidationDisabled)
+ {
+ await service.InstallPackageAsync("Root.Plugin", "2.0.0");
+ Assert.True(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+ }
+ else
+ {
+ var exception = await Assert.ThrowsAsync(
+ () => service.InstallPackageAsync("Root.Plugin", "2.0.0"));
+ Assert.Contains("WindowTranslator.Abstractions", exception.Message);
+ Assert.Contains("[2.0.0, 3.0.0)", exception.Message);
+ Assert.False(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+ Assert.Empty(await service.GetInstalledPackagesAsync());
+ }
}
finally
{
@@ -844,9 +828,8 @@ public async Task InstallAcceptsCompatibleHostAbstractionsWithoutDownloadingIt()
["lib/net10.0/Root.Plugin.dll"] = "root"u8.ToArray(),
}));
- using var client = new HttpClient(handler);
using var service = CreateService(
- client,
+ handler,
testDirectory,
new Dictionary(StringComparer.OrdinalIgnoreCase)
{
@@ -888,8 +871,7 @@ public async Task InstallRejectsPackageWithoutDirectAbstractionsDependency()
},
includeAbstractionsDependency: false));
- using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory);
+ using var service = CreateService(handler, testDirectory);
var exception = await Assert.ThrowsAsync(
() => service.InstallPackageAsync("Root.Plugin", "1.0.0"));
@@ -924,8 +906,7 @@ public async Task InstallRejectsPackageWithoutPluginTag()
},
includePluginTag: false));
- using var client = new HttpClient(handler);
- using var service = CreateService(client, testDirectory);
+ using var service = CreateService(handler, testDirectory);
var exception = await Assert.ThrowsAsync(
() => service.InstallPackageAsync("Root.Plugin", "1.0.0"));
@@ -972,20 +953,17 @@ public async Task SelectedPackageLoadsReadmeForTheSelectedReleaseChannel()
["README.md"] = "# Preview README"u8.ToArray(),
}));
- var metadataSource = new InMemoryNuGetMetadataSource();
- metadataSource.AddReadmeUrl(
+ handler.AddReadmeUrl(
"Readme.Plugin",
"1.0.0",
"https://nuget.test/readme/readme.plugin/1.0.0");
- metadataSource.AddReadmeUrl(
+ handler.AddReadmeUrl(
"Readme.Plugin",
"2.0.0-preview.1",
"https://nuget.test/readme/readme.plugin/2.0.0-preview.1");
- using var client = new HttpClient(handler);
using var service = CreateService(
- client,
- testDirectory,
- metadataSource: metadataSource);
+ handler,
+ testDirectory);
var viewModel = new PluginStoreViewModel(
service,
NullLogger.Instance,
@@ -1154,7 +1132,7 @@ public void CatalogSynchronizationClearsStaleFilesWhenSourceIsMissing()
}
[Fact]
- public void CatalogSynchronizationExcludesPackagesFromAnotherHostMajorVersion()
+ public void CatalogSynchronizationFollowsCompatibilityValidationSetting()
{
var sourceDirectory = CreateTestDirectory();
var destinationDirectory = CreateTestDirectory();
@@ -1190,10 +1168,12 @@ public void CatalogSynchronizationExcludesPackagesFromAnotherHostMajorVersion()
destinationDirectory,
"Compatible.Plugin",
"Compatible.Plugin.dll")));
- Assert.False(Directory.Exists(Path.Combine(
- destinationDirectory,
- "Incompatible.Plugin")));
- Assert.Equal(["Incompatible.Plugin"], incompatiblePackages);
+ Assert.Equal(
+ PluginCompatibility.ValidationDisabled,
+ Directory.Exists(Path.Combine(destinationDirectory, "Incompatible.Plugin")));
+ Assert.Equal(
+ PluginCompatibility.ValidationDisabled ? [] : ["Incompatible.Plugin"],
+ incompatiblePackages);
}
finally
{
@@ -1373,20 +1353,19 @@ public void FrameworkSelectionPrefersTheCompatibleWindowsTarget()
}
private static NuGetPluginService CreateService(
- HttpClient client,
+ InMemoryNuGetHandler handler,
string pluginDirectory,
IReadOnlyDictionary? hostPackageVersions = null,
- INuGetPluginMetadataSource? metadataSource = null,
int? hostMajorVersion = null)
=> new(
NullLogger.Instance,
- client,
+ new InMemoryHttpClientFactory(handler),
+ handler.CreateRepository(),
pluginDirectory,
- hostPackageVersions: hostPackageVersions,
- metadataSource: metadataSource ?? new InMemoryNuGetMetadataSource(),
- hostMajorVersion: hostMajorVersion);
+ hostPackageVersions ?? NuGetPluginService.CreateHostPackageVersions(),
+ hostMajorVersion ?? AppInfo.Instance.Version.Major);
- private static NuGetPluginVersionMetadata CreatePluginVersionMetadata(
+ private static TestPackageVersion CreatePluginVersionMetadata(
string version,
string? abstractionsRange = null,
bool includeAbstractionsDependency = true)
@@ -1407,6 +1386,32 @@ abstractionsRange is null
: []),
]);
+ private static IPackageSearchMetadata CreatePackageSearchMetadata(
+ string packageId,
+ string? title,
+ string? description,
+ string? authors,
+ string? projectUrl,
+ string? licenseUrl,
+ NuGetVersion? version = null,
+ IEnumerable? dependencySets = null,
+ bool isListed = true,
+ string? readmeFileUrl = null)
+ => new TestPackageSearchMetadata
+ {
+ Identity = new PackageIdentity(
+ packageId,
+ version ?? NuGetVersion.Parse("0.0.0")),
+ Title = title!,
+ Description = description!,
+ Authors = authors!,
+ ProjectUrl = projectUrl is null ? null! : new Uri(projectUrl),
+ LicenseUrl = licenseUrl is null ? null! : new Uri(licenseUrl),
+ DependencySets = dependencySets ?? [],
+ IsListed = isListed,
+ ReadmeFileUrl = readmeFileUrl!,
+ };
+
private static async Task WaitForReadmeAsync(
PluginPackageViewModel package,
string expectedReadme)
@@ -1542,73 +1547,90 @@ private static void DeleteTestDirectory(string path)
private sealed record TestDependency(string Id, string Version, string? Exclude = null);
- private sealed class InMemoryNuGetMetadataSource : INuGetPluginMetadataSource
+ private sealed record TestPackageVersion(
+ NuGetVersion Version,
+ bool IsListed,
+ IReadOnlyList DependencyGroups);
+
+ private sealed class TestPackageSearchMetadata : IPackageSearchMetadata
+ {
+ public string Authors { get; init; } = null!;
+ public IEnumerable DependencySets { get; init; } = [];
+ public string Description { get; init; } = null!;
+ public long? DownloadCount { get; init; }
+ public Uri IconUrl { get; init; } = null!;
+ public PackageIdentity Identity { get; init; } = null!;
+ public Uri LicenseUrl { get; init; } = null!;
+ public Uri ProjectUrl { get; init; } = null!;
+ public Uri ReadmeUrl { get; init; } = null!;
+ public string ReadmeFileUrl { get; init; } = null!;
+ public Uri ReportAbuseUrl { get; init; } = null!;
+ public Uri PackageDetailsUrl { get; init; } = null!;
+ public DateTimeOffset? Published { get; init; }
+ public IReadOnlyList OwnersList { get; init; } = [];
+ public string Owners { get; init; } = null!;
+ public bool RequireLicenseAcceptance { get; init; }
+ public string Summary { get; init; } = null!;
+ public string Tags { get; init; } = null!;
+ public string Title { get; init; } = null!;
+ public bool IsListed { get; init; }
+ public bool PrefixReserved { get; init; }
+ public LicenseMetadata LicenseMetadata { get; init; } = null!;
+ public IEnumerable Vulnerabilities { get; init; } = [];
+
+ public Task GetDeprecationMetadataAsync()
+ => Task.FromResult(null);
+
+ public Task> GetVersionsAsync()
+ => Task.FromResult>([]);
+ }
+
+ private sealed class InMemoryHttpClientFactory(InMemoryNuGetHandler handler) : IHttpClientFactory
+ {
+ public HttpClient CreateClient(string name) => new(handler, disposeHandler: false);
+ }
+
+ private sealed class InMemoryNuGetHandler : HttpMessageHandler
{
- private readonly Dictionary> versions =
+ private readonly Dictionary<(string Id, string Version), byte[]> packages = new();
+ private readonly Dictionary> metadataVersions =
new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary readmeUrls =
new(StringComparer.OrdinalIgnoreCase);
- public IReadOnlyList SearchResults { get; init; } = [];
+ public IReadOnlyList SearchResults { get; set; } = [];
- public Exception? SearchException { get; init; }
+ public Exception? SearchException { get; set; }
- public List RequestedTags { get; } = [];
+ public List RequestedSearchTerms { get; } = [];
public List RequestedPrereleaseOptions { get; } = [];
- public void AddVersions(string packageId, params NuGetPluginVersionMetadata[] packageVersions)
- => this.versions[packageId] = packageVersions;
+ public List RequestedPaths { get; } = [];
- public void AddReadmeUrl(string packageId, string version, string url)
- => this.readmeUrls[GetReadmeKey(packageId, NuGetVersion.Parse(version))] = url;
+ public void AddPackage(string id, string version, byte[] package)
+ => this.packages[(NormalizeId(id), NormalizeVersion(version))] = package;
- public Task> SearchAsync(
- string tag,
- bool includePrerelease,
- CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
- this.RequestedTags.Add(tag);
- this.RequestedPrereleaseOptions.Add(includePrerelease);
- return this.SearchException is null
- ? Task.FromResult(this.SearchResults)
- : Task.FromException>(this.SearchException);
- }
+ public void AddMetadataVersions(string packageId, params TestPackageVersion[] packageVersions)
+ => this.metadataVersions[packageId] = packageVersions;
- public Task> GetPackageVersionsAsync(
- string packageId,
- CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
- return Task.FromResult(
- this.versions.TryGetValue(packageId, out var packageVersions)
- ? packageVersions
- : (IReadOnlyList)[]);
- }
+ public void AddReadmeUrl(string packageId, string version, string url)
+ => this.readmeUrls[GetReadmeKey(packageId, NuGetVersion.Parse(version))] = url;
- public Task GetReadmeUrlAsync(
- string packageId,
- NuGetVersion version,
- CancellationToken cancellationToken)
- {
- cancellationToken.ThrowIfCancellationRequested();
- this.readmeUrls.TryGetValue(GetReadmeKey(packageId, version), out var readmeUrl);
- return Task.FromResult(readmeUrl);
- }
+ public SourceRepository CreateRepository()
+ => new(
+ new PackageSource("https://nuget.test/v3/index.json"),
+ [
+ new InMemoryResourceProvider(
+ new InMemoryPackageSearchResource(this)),
+ new InMemoryResourceProvider(
+ new InMemoryPackageMetadataResource(this)),
+ new InMemoryResourceProvider(
+ new InMemoryFindPackageByIdResource(this)),
+ ]);
private static string GetReadmeKey(string packageId, NuGetVersion version)
=> $"{packageId}\n{version.ToNormalizedString()}";
- }
-
- private sealed class InMemoryNuGetHandler : HttpMessageHandler
- {
- private readonly Dictionary<(string Id, string Version), byte[]> packages = new();
-
- public List RequestedPaths { get; } = [];
-
- public void AddPackage(string id, string version, byte[] package)
- => this.packages[(id.ToLowerInvariant(), version.ToLowerInvariant())] = package;
protected override Task SendAsync(
HttpRequestMessage request,
@@ -1624,46 +1646,13 @@ protected override Task SendAsync(
return Task.FromResult(CreateReadmeResponse(segments[1], segments[2]));
}
- if (segments.Length == 3
- && segments[0].Equals("v3-flatcontainer", StringComparison.OrdinalIgnoreCase)
- && segments[2].Equals("index.json", StringComparison.OrdinalIgnoreCase))
- {
- var id = segments[1].ToLowerInvariant();
- var versions = this.packages.Keys
- .Where(key => key.Id == id)
- .Select(key => key.Version)
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .OrderBy(version => version, StringComparer.OrdinalIgnoreCase)
- .ToArray();
- return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new StringContent(
- JsonSerializer.Serialize(new { versions }),
- Encoding.UTF8,
- "application/json"),
- });
- }
-
- if (segments.Length == 4
- && segments[0].Equals("v3-flatcontainer", StringComparison.OrdinalIgnoreCase))
- {
- var key = (segments[1].ToLowerInvariant(), segments[2].ToLowerInvariant());
- if (this.packages.TryGetValue(key, out var package))
- {
- return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
- {
- Content = new ByteArrayContent(package),
- });
- }
- }
-
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
}
private HttpResponseMessage CreateReadmeResponse(string packageId, string version)
{
if (!this.packages.TryGetValue(
- (packageId.ToLowerInvariant(), version.ToLowerInvariant()),
+ (NormalizeId(packageId), NormalizeVersion(version)),
out var package))
{
return new HttpResponseMessage(HttpStatusCode.NotFound);
@@ -1685,6 +1674,157 @@ private HttpResponseMessage CreateReadmeResponse(string packageId, string versio
};
}
+ private static string NormalizeId(string packageId) => packageId.ToLowerInvariant();
+
+ private static string NormalizeVersion(string version)
+ => NuGetVersion.Parse(version).ToNormalizedString().ToLowerInvariant();
+
+ private sealed class InMemoryPackageSearchResource(InMemoryNuGetHandler source)
+ : PackageSearchResource
+ {
+ public override Task> SearchAsync(
+ string searchTerm,
+ SearchFilter filters,
+ int skip,
+ int take,
+ NuGet.Common.ILogger log,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ source.RequestedSearchTerms.Add(searchTerm);
+ source.RequestedPrereleaseOptions.Add(filters.IncludePrerelease);
+ return source.SearchException is null
+ ? Task.FromResult(source.SearchResults.Skip(skip).Take(take).AsEnumerable())
+ : Task.FromException>(source.SearchException);
+ }
+ }
+
+ private sealed class InMemoryPackageMetadataResource(InMemoryNuGetHandler source)
+ : PackageMetadataResource
+ {
+ public override Task> GetMetadataAsync(
+ string packageId,
+ bool includePrerelease,
+ bool includeUnlisted,
+ SourceCacheContext sourceCacheContext,
+ NuGet.Common.ILogger log,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ var versions = source.metadataVersions.TryGetValue(packageId, out var packageVersions)
+ ? packageVersions
+ : [];
+ var metadata = versions
+ .Where(version => includePrerelease || !version.Version.IsPrerelease)
+ .Where(version => includeUnlisted || version.IsListed)
+ .Select(version => CreatePackageSearchMetadata(
+ packageId,
+ title: null,
+ description: null,
+ authors: null,
+ projectUrl: null,
+ licenseUrl: null,
+ version.Version,
+ version.DependencyGroups,
+ version.IsListed));
+ return Task.FromResult(metadata);
+ }
+
+ public override Task GetMetadataAsync(
+ PackageIdentity identity,
+ SourceCacheContext sourceCacheContext,
+ NuGet.Common.ILogger log,
+ CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ source.readmeUrls.TryGetValue(GetReadmeKey(identity.Id, identity.Version), out var readmeUrl);
+ return Task.FromResult(CreatePackageSearchMetadata(
+ identity.Id,
+ title: null,
+ description: null,
+ authors: null,
+ projectUrl: null,
+ licenseUrl: null,
+ identity.Version,
+ readmeFileUrl: readmeUrl));
+ }
+ }
+
+ private sealed class InMemoryFindPackageByIdResource(InMemoryNuGetHandler source)
+ : FindPackageByIdResource
+ {
+ public override Task> GetAllVersionsAsync(
+ string id,
+ SourceCacheContext cacheContext,
+ NuGet.Common.ILogger logger,
+ CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+ var normalizedId = NormalizeId(id);
+ source.RequestedPaths.Add($"/v3-flatcontainer/{normalizedId}/index.json");
+ return Task.FromResult(source.packages.Keys
+ .Where(key => key.Id == normalizedId)
+ .Select(key => NuGetVersion.Parse(key.Version))
+ .OrderBy(version => version)
+ .AsEnumerable());
+ }
+
+ public override async Task CopyNupkgToStreamAsync(
+ string id,
+ NuGetVersion version,
+ Stream destination,
+ SourceCacheContext cacheContext,
+ NuGet.Common.ILogger logger,
+ CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+ var normalizedId = NormalizeId(id);
+ var normalizedVersion = NormalizeVersion(version.ToNormalizedString());
+ source.RequestedPaths.Add(
+ $"/v3-flatcontainer/{normalizedId}/{normalizedVersion}/{normalizedId}.{normalizedVersion}.nupkg");
+ if (!source.packages.TryGetValue((normalizedId, normalizedVersion), out var package))
+ {
+ return false;
+ }
+
+ await destination.WriteAsync(package, token);
+ return true;
+ }
+
+ public override Task GetDependencyInfoAsync(
+ string id,
+ NuGetVersion version,
+ SourceCacheContext cacheContext,
+ NuGet.Common.ILogger logger,
+ CancellationToken token)
+ => throw new NotSupportedException();
+
+ public override Task GetPackageDownloaderAsync(
+ PackageIdentity packageIdentity,
+ SourceCacheContext cacheContext,
+ NuGet.Common.ILogger logger,
+ CancellationToken token)
+ => throw new NotSupportedException();
+
+ public override Task DoesPackageExistAsync(
+ string id,
+ NuGetVersion version,
+ SourceCacheContext cacheContext,
+ NuGet.Common.ILogger logger,
+ CancellationToken token)
+ => Task.FromResult(source.packages.ContainsKey(
+ (NormalizeId(id), NormalizeVersion(version.ToNormalizedString()))));
+ }
+ }
+
+ private sealed class InMemoryResourceProvider(TResource resource)
+ : ResourceProvider(typeof(TResource))
+ where TResource : class, INuGetResource
+ {
+ public override Task> TryCreate(
+ SourceRepository source,
+ CancellationToken token)
+ => Task.FromResult(Tuple.Create(true, resource));
}
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
index 3eda3e27..0b1865c5 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
@@ -1,15 +1,13 @@
using System.IO;
using System.IO.Compression;
-using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using NuGet.Frameworks;
using NuGet.Packaging;
using NuGet.Packaging.Core;
+using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
namespace WindowTranslator.Modules.PluginStore;
@@ -18,22 +16,19 @@ namespace WindowTranslator.Modules.PluginStore;
/// NuGetパッケージとそのランタイム依存関係を、プラグインフォルダへ展開します。
///
internal sealed class NuGetPackageInstaller(
- HttpClient httpClient,
+ FindPackageByIdResource packageResource,
ILogger logger,
- IReadOnlyDictionary? hostPackageVersions = null)
+ IReadOnlyDictionary hostPackageVersions)
{
- private const string FlatContainerBase = "https://api.nuget.org/v3-flatcontainer";
-
private static readonly NuGetFramework HostFramework = GetHostFramework();
private static readonly FrameworkReducer FrameworkReducer = new();
private static readonly string[] CompatibleRuntimeIdentifiers =
[RuntimeInformation.RuntimeIdentifier, "win", "any"];
- private readonly HttpClient httpClient = httpClient;
+ private readonly FindPackageByIdResource packageResource = packageResource;
private readonly ILogger logger = logger;
- private readonly IReadOnlyDictionary hostPackageVersions =
- hostPackageVersions ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly IReadOnlyDictionary hostPackageVersions = hostPackageVersions;
public async Task InstallAsync(
string packageId,
@@ -151,6 +146,7 @@ private async Task> ResolvePackageGraphAsyn
var artifacts = new Dictionary(StringComparer.OrdinalIgnoreCase);
var queue = new Queue();
var queued = new HashSet(StringComparer.OrdinalIgnoreCase);
+ using var cacheContext = new SourceCacheContext();
AddConstraint(
rootPackageId,
@@ -181,7 +177,11 @@ private async Task> ResolvePackageGraphAsyn
var ranges = currentConstraints.Select(c => c.Range).ToArray();
var resolvedVersion = currentId.Equals(rootPackageId, StringComparison.OrdinalIgnoreCase)
? rootVersion
- : await ResolveDependencyVersionAsync(currentId, ranges, cancellationToken).ConfigureAwait(false);
+ : await ResolveDependencyVersionAsync(
+ currentId,
+ ranges,
+ cacheContext,
+ cancellationToken).ConfigureAwait(false);
if (!ranges.All(r => r.Satisfies(resolvedVersion)))
{
@@ -208,6 +208,7 @@ await DownloadPackageAsync(
resolvedVersion,
packagePath,
currentId.Equals(rootPackageId, StringComparison.OrdinalIgnoreCase) ? progress : null,
+ cacheContext,
cancellationToken).ConfigureAwait(false);
ValidateHostPackageDependencies(
packagePath,
@@ -274,21 +275,18 @@ void Enqueue(string id)
private async Task ResolveDependencyVersionAsync(
string packageId,
IReadOnlyCollection ranges,
+ SourceCacheContext cacheContext,
CancellationToken cancellationToken)
{
- var versionsUrl = $"{FlatContainerBase}/{packageId.ToLowerInvariant()}/index.json";
- using var response = await this.httpClient.GetAsync(versionsUrl, cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
- await using var content = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
- var versionList = await JsonSerializer.DeserializeAsync(
- content,
- cancellationToken: cancellationToken).ConfigureAwait(false);
-
- var compatibleVersions = versionList?.Versions?
- .Select(NuGetVersion.Parse)
+ var versions = await this.packageResource.GetAllVersionsAsync(
+ packageId,
+ cacheContext,
+ NuGet.Common.NullLogger.Instance,
+ cancellationToken).ConfigureAwait(false);
+ var compatibleVersions = versions
.Where(v => ranges.All(r => r.Satisfies(v)))
.OrderBy(v => v)
- .ToArray() ?? [];
+ .ToArray();
return compatibleVersions.FirstOrDefault(v => !v.IsPrerelease)
?? compatibleVersions.FirstOrDefault()
?? throw new InvalidOperationException(
@@ -300,34 +298,24 @@ private async Task DownloadPackageAsync(
NuGetVersion version,
string destinationPath,
IProgress? progress,
+ SourceCacheContext cacheContext,
CancellationToken cancellationToken)
{
- var packageIdLower = packageId.ToLowerInvariant();
- var versionLower = version.ToNormalizedString().ToLowerInvariant();
- var url = $"{FlatContainerBase}/{packageIdLower}/{versionLower}/{packageIdLower}.{versionLower}.nupkg";
this.logger.LogInformation("NuGetパッケージをダウンロード中: {PackageId} {Version}", packageId, version);
-
- using var response = await this.httpClient.GetAsync(
- url,
- HttpCompletionOption.ResponseHeadersRead,
- cancellationToken).ConfigureAwait(false);
- response.EnsureSuccessStatusCode();
-
- var totalBytes = response.Content.Headers.ContentLength ?? -1;
- var downloadedBytes = 0L;
- await using var source = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
+ progress?.Report(0);
await using var destination = File.Create(destinationPath);
- var buffer = new byte[81920];
- int bytesRead;
- while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) > 0)
+ var copied = await this.packageResource.CopyNupkgToStreamAsync(
+ packageId,
+ version,
+ destination,
+ cacheContext,
+ NuGet.Common.NullLogger.Instance,
+ cancellationToken).ConfigureAwait(false);
+ if (!copied)
{
- await destination.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken).ConfigureAwait(false);
- downloadedBytes += bytesRead;
- if (totalBytes > 0)
- {
- progress?.Report((double)downloadedBytes / totalBytes);
- }
+ throw new InvalidOperationException($"NuGetパッケージを取得できませんでした: {packageId} {version}");
}
+ progress?.Report(1);
}
private static List ReadRuntimeDependencies(string packagePath)
@@ -381,7 +369,8 @@ private static void ValidateHostPackageDependencies(
$"パッケージ {packageId} {packageVersion} はWindowTranslatorプラグインタグを持っていません。");
}
- if (!hostPackageVersions.ContainsKey(NuGetPluginService.AbstractionsPackageId))
+ if (!PluginCompatibility.ValidationDisabled
+ && !hostPackageVersions.ContainsKey(NuGetPluginService.AbstractionsPackageId))
{
throw new InvalidOperationException(
$"実行中の{NuGetPluginService.AbstractionsPackageId}のバージョンを確認できません。");
@@ -400,7 +389,7 @@ private static void ValidateHostPackageDependencies(
foreach (var dependency in dependencies)
{
if (!hostPackageVersions.TryGetValue(dependency.Id, out var hostVersion)
- || dependency.VersionRange.Satisfies(hostVersion))
+ || PluginCompatibility.IsVersionCompatible(dependency.VersionRange, hostVersion))
{
continue;
}
@@ -628,6 +617,4 @@ private sealed record PackageArtifact(string Id, NuGetVersion Version, string Pa
private sealed record DependencyConstraint(string Source, VersionRange Range);
- private sealed record VersionIndex(
- [property: JsonPropertyName("versions")] string[]? Versions);
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 34636209..caad75ae 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -407,8 +407,9 @@ internal static IReadOnlySet GetIncompatiblePackageIds(
stream,
NuGetPluginService.ManifestJsonOptions);
return manifest?.Packages
- .Where(package => package.HostMajorVersion is not null
- && package.HostMajorVersion != hostMajorVersion)
+ .Where(package => !PluginCompatibility.IsHostMajorCompatible(
+ package.HostMajorVersion,
+ hostMajorVersion))
.Select(package => package.Id)
.ToHashSet(StringComparer.OrdinalIgnoreCase)
?? new HashSet(StringComparer.OrdinalIgnoreCase);
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index c3aa7de6..8e3bd218 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -6,19 +6,24 @@
using System.Text.Json.Serialization;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.Threading;
using NuGet.Packaging;
+using NuGet.Packaging.Core;
+using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
namespace WindowTranslator.Modules.PluginStore;
///
-/// NuGet V3 REST APIを使用してプラグインパッケージの検索・インストール・管理を行うサービスです。
+/// NuGetクライアントSDKを使用してプラグインパッケージの検索・インストール・管理を行うサービスです。
///
public sealed class NuGetPluginService : BackgroundService
{
- private const string NuGetServiceIndexUrl = "https://api.nuget.org/v3/index.json";
+ internal const string NuGetServiceIndexUrl = "https://api.nuget.org/v3/index.json";
+ internal const string HttpClientName = "NuGetPluginReadme";
internal const string PluginTag = "windowtranslator-plugin";
internal const string AbstractionsPackageId = "WindowTranslator.Abstractions";
+ private const int SearchResultLimit = 100;
private const int MaxConcurrentMetadataRequests = 8;
private static readonly TimeSpan PackageInformationRefreshInterval = TimeSpan.FromHours(1);
@@ -30,58 +35,34 @@ public sealed class NuGetPluginService : BackgroundService
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
};
- private readonly HttpClient httpClient;
+ private readonly IHttpClientFactory httpClientFactory;
+ private readonly SourceRepository repository;
private readonly ILogger logger;
private readonly string userPluginsDir;
private readonly string manifestPath;
- private readonly bool ownsHttpClient;
private readonly IReadOnlyDictionary hostPackageVersions;
- private readonly INuGetPluginMetadataSource metadataSource;
- private readonly App? app;
private readonly int hostMajorVersion;
- private readonly SemaphoreSlim operationLock = new(1, 1);
- private readonly SemaphoreSlim refreshLock = new(1, 1);
+ private readonly AsyncSemaphore operationLock = new(1);
+ private readonly AsyncSemaphore refreshLock = new(1);
private readonly object snapshotLock = new();
private PluginStoreSnapshot packageSnapshot = PluginStoreSnapshot.Empty;
private long installedPackagesGeneration;
- private int disposeState;
-
- public NuGetPluginService(ILogger logger, App app)
- : this(
- logger,
- new HttpClient(new HttpClientHandler
- {
- AutomaticDecompression = DecompressionMethods.All,
- })
- {
- Timeout = TimeSpan.FromSeconds(30),
- },
- Path.Combine(PathUtility.UserDir, "plugins"),
- ownsHttpClient: true,
- app: app)
- {
- }
internal NuGetPluginService(
ILogger logger,
- HttpClient httpClient,
+ IHttpClientFactory httpClientFactory,
+ SourceRepository repository,
string userPluginsDir,
- bool ownsHttpClient = false,
- IReadOnlyDictionary? hostPackageVersions = null,
- INuGetPluginMetadataSource? metadataSource = null,
- App? app = null,
- int? hostMajorVersion = null)
+ IReadOnlyDictionary hostPackageVersions,
+ int hostMajorVersion)
{
this.logger = logger;
- this.httpClient = httpClient;
+ this.httpClientFactory = httpClientFactory;
+ this.repository = repository;
this.userPluginsDir = Path.GetFullPath(userPluginsDir);
this.manifestPath = Path.Combine(this.userPluginsDir, "nuget-manifest.json");
- this.ownsHttpClient = ownsHttpClient;
- this.hostPackageVersions = hostPackageVersions ?? CreateHostPackageVersions();
- this.metadataSource = metadataSource
- ?? new NuGetProtocolPluginMetadataSource(NuGetServiceIndexUrl);
- this.app = app;
- this.hostMajorVersion = hostMajorVersion ?? AppInfo.Instance.Version.Major;
+ this.hostPackageVersions = hostPackageVersions;
+ this.hostMajorVersion = hostMajorVersion;
}
internal event EventHandler? PackageInformationUpdated;
@@ -99,11 +80,6 @@ internal PluginStoreSnapshot PackageSnapshot
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
- if (this.app is not null)
- {
- await this.app.WaitForStartupAsync().ConfigureAwait(false);
- }
-
while (!stoppingToken.IsCancellationRequested)
{
await RefreshPackageInformationAsync(stoppingToken).ConfigureAwait(false);
@@ -113,52 +89,45 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
internal async Task RefreshPackageInformationAsync(CancellationToken cancellationToken = default)
{
- await this.refreshLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ using var refresh = await this.refreshLock.EnterAsync(cancellationToken);
+ var previousSnapshot = this.PackageSnapshot;
+ var packages = previousSnapshot.Packages;
+ Exception? error = null;
try
{
- var previousSnapshot = this.PackageSnapshot;
- var packages = previousSnapshot.Packages;
- Exception? error = null;
- try
- {
- packages = await SearchPackagesAsync(cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex) when (ex is not OperationCanceledException)
- {
- this.logger.LogWarning(ex, "NuGetからプラグイン情報を更新できませんでした。");
- error = ex;
- }
-
- var installedGenerationBefore = Volatile.Read(ref this.installedPackagesGeneration);
- IReadOnlyList installedPackages;
- try
- {
- installedPackages = await GetInstalledPackagesAsync(cancellationToken).ConfigureAwait(false);
- }
- catch (Exception ex) when (ex is not OperationCanceledException)
- {
- this.logger.LogWarning(ex, "インストール済みプラグイン情報を更新できませんでした。");
- installedPackages = this.PackageSnapshot.InstalledPackages;
- error = ex;
- }
+ packages = await SearchPackagesAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ this.logger.LogWarning(ex, "NuGetからプラグイン情報を更新できませんでした。");
+ error = ex;
+ }
- var installedGenerationAfter = Volatile.Read(ref this.installedPackagesGeneration);
- if (installedGenerationBefore != installedGenerationAfter)
- {
- installedPackages = this.PackageSnapshot.InstalledPackages;
- }
- SetPackageSnapshot(
- new(
- IsInitialized: true,
- InstalledPackages: installedPackages,
- Packages: packages,
- Error: error),
- installedGenerationAfter);
+ var installedGenerationBefore = Volatile.Read(ref this.installedPackagesGeneration);
+ IReadOnlyList installedPackages;
+ try
+ {
+ installedPackages = await GetInstalledPackagesAsync(cancellationToken).ConfigureAwait(false);
}
- finally
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ this.logger.LogWarning(ex, "インストール済みプラグイン情報を更新できませんでした。");
+ installedPackages = this.PackageSnapshot.InstalledPackages;
+ error = ex;
+ }
+
+ var installedGenerationAfter = Volatile.Read(ref this.installedPackagesGeneration);
+ if (installedGenerationBefore != installedGenerationAfter)
{
- this.refreshLock.Release();
+ installedPackages = this.PackageSnapshot.InstalledPackages;
}
+ SetPackageSnapshot(
+ new(
+ IsInitialized: true,
+ InstalledPackages: installedPackages,
+ Packages: packages,
+ Error: error),
+ installedGenerationAfter);
}
///
@@ -166,19 +135,32 @@ internal async Task RefreshPackageInformationAsync(CancellationToken cancellatio
///
public async Task> SearchPackagesAsync(CancellationToken cancellationToken = default)
{
- var searchResults = await this.metadataSource
- .SearchAsync(PluginTag, includePrerelease: true, cancellationToken)
+ var searchResource = await this.repository
+ .GetResourceAsync(cancellationToken)
.ConfigureAwait(false);
- this.logger.LogInformation("NuGetタグ検索完了: {Count}件の候補が見つかりました。", searchResults.Count);
+ var metadataResource = await this.repository
+ .GetResourceAsync(cancellationToken)
+ .ConfigureAwait(false);
+ var searchResults = (await searchResource.SearchAsync(
+ $"tags:{PluginTag}",
+ new(includePrerelease: true) { IncludeDelisted = false },
+ skip: 0,
+ take: SearchResultLimit,
+ NuGet.Common.NullLogger.Instance,
+ cancellationToken).ConfigureAwait(false))
+ .Where(metadata => !string.IsNullOrWhiteSpace(metadata.Identity?.Id))
+ .ToArray();
+ this.logger.LogInformation("NuGetタグ検索完了: {Count}件の候補が見つかりました。", searchResults.Length);
- using var requestGate = new SemaphoreSlim(MaxConcurrentMetadataRequests);
+ var requestGate = new AsyncSemaphore(MaxConcurrentMetadataRequests);
var packageTasks = searchResults.Select(async data =>
{
- await requestGate.WaitAsync(cancellationToken).ConfigureAwait(false);
+ using var request = await requestGate.EnterAsync(cancellationToken);
try
{
return await CreateCompatiblePackageInfoAsync(
data,
+ metadataResource,
cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
@@ -190,13 +172,9 @@ public async Task> SearchPackagesAsync(Cancellat
this.logger.LogWarning(
ex,
"NuGetパッケージのプラグイン互換性を確認できなかったため除外します: {PackageId}",
- data.Id);
+ data.Identity.Id);
return null;
}
- finally
- {
- requestGate.Release();
- }
});
var packages = await Task.WhenAll(packageTasks).ConfigureAwait(false);
var compatiblePackages = packages.Where(package => package is not null).Select(package => package!).ToArray();
@@ -224,14 +202,23 @@ public async Task> SearchPackagesAsync(Cancellat
throw new ArgumentException($"不正なNuGetパッケージバージョンです: {version}", nameof(version));
}
- var readmeUrl = await this.metadataSource
- .GetReadmeUrlAsync(packageId, packageVersion, cancellationToken)
+ var metadataResource = await this.repository
+ .GetResourceAsync(cancellationToken)
.ConfigureAwait(false);
+ using var cacheContext = new SourceCacheContext();
+ var metadata = await metadataResource.GetMetadataAsync(
+ new PackageIdentity(packageId, packageVersion),
+ cacheContext,
+ NuGet.Common.NullLogger.Instance,
+ cancellationToken).ConfigureAwait(false);
+ var readmeUrl = metadata?.ReadmeFileUrl;
if (string.IsNullOrWhiteSpace(readmeUrl))
{
return null;
}
- using var response = await this.httpClient.GetAsync(readmeUrl, cancellationToken).ConfigureAwait(false);
+
+ using var httpClient = this.httpClientFactory.CreateClient(HttpClientName);
+ using var response = await httpClient.GetAsync(readmeUrl, cancellationToken).ConfigureAwait(false);
if (response.StatusCode == HttpStatusCode.NotFound)
{
return null;
@@ -252,12 +239,15 @@ public async Task InstallPackageAsync(string packageId, string version, IProgres
var backupDir = $"{targetDir}.backup-{operationId}";
var targetMoved = false;
var stagingMoved = false;
- await this.operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
+ using var operation = await this.operationLock.EnterAsync(cancellationToken);
try
{
Directory.CreateDirectory(this.userPluginsDir);
+ var packageResource = await this.repository
+ .GetResourceAsync(cancellationToken)
+ .ConfigureAwait(false);
var installer = new NuGetPackageInstaller(
- this.httpClient,
+ packageResource,
this.logger,
this.hostPackageVersions);
await installer.InstallAsync(
@@ -324,7 +314,6 @@ await installer.InstallAsync(
finally
{
TryDeleteDirectory(stagingDir);
- this.operationLock.Release();
}
}
@@ -338,70 +327,63 @@ public async Task UninstallPackageAsync(string packageId, CancellationToken canc
var targetDir = GetPackageDirectory(packageId);
var uninstallingDir = $"{targetDir}.uninstalling-{operationId}";
var targetMoved = false;
- await this.operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- this.logger.LogInformation("パッケージをアンインストール: {PackageId}", packageId);
- Directory.CreateDirectory(this.userPluginsDir);
+ using var operation = await this.operationLock.EnterAsync(cancellationToken);
+ this.logger.LogInformation("パッケージをアンインストール: {PackageId}", packageId);
+ Directory.CreateDirectory(this.userPluginsDir);
- var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
- var updatedManifest = RemovePackage(manifest, packageId);
+ var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ var updatedManifest = RemovePackage(manifest, packageId);
- if (Directory.Exists(targetDir))
- {
- Directory.Move(targetDir, uninstallingDir);
- targetMoved = true;
- }
+ if (Directory.Exists(targetDir))
+ {
+ Directory.Move(targetDir, uninstallingDir);
+ targetMoved = true;
+ }
+ try
+ {
+ await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
+ UpdateInstalledPackages(updatedManifest.Packages);
+ }
+ catch
+ {
try
{
- await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
- UpdateInstalledPackages(updatedManifest.Packages);
- }
- catch
- {
- try
- {
- if (targetMoved
- && Directory.Exists(uninstallingDir)
- && !Directory.Exists(targetDir))
- {
- Directory.Move(uninstallingDir, targetDir);
- }
- }
- catch (Exception rollbackException)
+ if (targetMoved
+ && Directory.Exists(uninstallingDir)
+ && !Directory.Exists(targetDir))
{
- this.logger.LogError(
- rollbackException,
- "プラグイン {PackageId} のアンインストール失敗後の復旧に失敗しました。",
- packageId);
+ Directory.Move(uninstallingDir, targetDir);
}
- throw;
}
-
- try
+ catch (Exception rollbackException)
{
- if (Directory.Exists(uninstallingDir))
- {
- Directory.Delete(uninstallingDir, recursive: true);
- }
+ this.logger.LogError(
+ rollbackException,
+ "プラグイン {PackageId} のアンインストール失敗後の復旧に失敗しました。",
+ packageId);
}
- catch (Exception ex)
+ throw;
+ }
+
+ try
+ {
+ if (Directory.Exists(uninstallingDir))
{
- this.logger.LogWarning(
- ex,
- "アンインストール済みプラグインフォルダの削除に失敗しました: {Directory}",
- uninstallingDir);
+ Directory.Delete(uninstallingDir, recursive: true);
}
-
- this.logger.LogInformation(
- "パッケージ {PackageId} を管理フォルダからアンインストールしました。再起動後に反映されます。",
- packageId);
}
- finally
+ catch (Exception ex)
{
- this.operationLock.Release();
+ this.logger.LogWarning(
+ ex,
+ "アンインストール済みプラグインフォルダの削除に失敗しました: {Directory}",
+ uninstallingDir);
}
+
+ this.logger.LogInformation(
+ "パッケージ {PackageId} を管理フォルダからアンインストールしました。再起動後に反映されます。",
+ packageId);
}
///
@@ -409,73 +391,69 @@ public async Task UninstallPackageAsync(string packageId, CancellationToken canc
///
public async Task> GetInstalledPackagesAsync(CancellationToken cancellationToken = default)
{
- await this.operationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
+ using var operation = await this.operationLock.EnterAsync(cancellationToken);
+ var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ var migratedPackages = manifest.Packages
+ .Select(package => package.HostMajorVersion is null
+ ? package with { HostMajorVersion = this.hostMajorVersion }
+ : package)
+ .ToList();
+ if (migratedPackages.Where((package, index) =>
+ package != manifest.Packages[index]).Any())
{
- var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
- var migratedPackages = manifest.Packages
- .Select(package => package.HostMajorVersion is null
- ? package with { HostMajorVersion = this.hostMajorVersion }
- : package)
- .ToList();
- if (migratedPackages.Where((package, index) =>
- package != manifest.Packages[index]).Any())
- {
- manifest = new InstalledManifest(migratedPackages);
- await SaveManifestAsync(manifest, cancellationToken).ConfigureAwait(false);
- }
-
- return GetCompatibilityAwarePackages(manifest.Packages);
- }
- finally
- {
- this.operationLock.Release();
+ manifest = new InstalledManifest(migratedPackages);
+ await SaveManifestAsync(manifest, cancellationToken).ConfigureAwait(false);
}
+
+ return GetCompatibilityAwarePackages(manifest.Packages);
}
private async Task CreateCompatiblePackageInfoAsync(
- NuGetPluginSearchMetadata data,
+ IPackageSearchMetadata data,
+ PackageMetadataResource metadataResource,
CancellationToken cancellationToken)
{
- var versions = await this.metadataSource
- .GetPackageVersionsAsync(data.Id, cancellationToken)
- .ConfigureAwait(false);
+ var packageId = data.Identity.Id;
+ using var cacheContext = new SourceCacheContext();
+ var versions = await metadataResource.GetMetadataAsync(
+ packageId,
+ includePrerelease: true,
+ includeUnlisted: false,
+ cacheContext,
+ NuGet.Common.NullLogger.Instance,
+ cancellationToken).ConfigureAwait(false);
var compatibleVersions = versions
- .Where(version => version.IsListed
- && HasCompatibleAbstractionsDependency(version.DependencyGroups))
- .OrderBy(version => version.Version)
+ .Where(version => version.Identity?.Version is not null
+ && version.IsListed
+ && HasCompatibleAbstractionsDependency(version.DependencySets))
+ .OrderBy(version => version.Identity.Version)
.ToArray();
if (compatibleVersions.Length == 0)
{
this.logger.LogDebug(
"WindowTranslator.Abstractionsへの互換依存がないため除外します: {PackageId}",
- data.Id);
+ packageId);
return null;
}
- var latestVersion = compatibleVersions[^1].Version.ToNormalizedString();
+ var latestVersion = compatibleVersions[^1].Identity.Version.ToNormalizedString();
return new NuGetPackageInfo(
- Id: data.Id,
+ Id: packageId,
Version: latestVersion,
- Title: data.Title ?? data.Id,
+ Title: data.Title ?? packageId,
Description: data.Description ?? string.Empty,
Authors: data.Authors ?? string.Empty,
- ProjectUrl: data.ProjectUrl,
- LicenseUrl: data.LicenseUrl,
+ ProjectUrl: data.ProjectUrl?.AbsoluteUri,
+ LicenseUrl: data.LicenseUrl?.AbsoluteUri,
Versions: compatibleVersions
- .Select(version => version.Version.ToNormalizedString())
+ .Select(version => version.Identity.Version.ToNormalizedString())
.ToArray());
}
private bool HasCompatibleAbstractionsDependency(
- IReadOnlyList dependencyGroups)
+ IEnumerable? dependencyGroups)
{
- if (!this.hostPackageVersions.TryGetValue(AbstractionsPackageId, out var hostVersion))
- {
- return false;
- }
-
- var dependencyGroup = NuGetPackageInstaller.SelectBestDependencyGroup(dependencyGroups);
+ var dependencyGroup = NuGetPackageInstaller.SelectBestDependencyGroup(dependencyGroups ?? []);
var dependency = dependencyGroup?.Packages.FirstOrDefault(item =>
item.Id.Equals(AbstractionsPackageId, StringComparison.OrdinalIgnoreCase));
if (dependency is null)
@@ -483,7 +461,8 @@ private bool HasCompatibleAbstractionsDependency(
return false;
}
- return dependency.VersionRange?.Satisfies(hostVersion) is not false;
+ this.hostPackageVersions.TryGetValue(AbstractionsPackageId, out var hostVersion);
+ return PluginCompatibility.IsVersionCompatible(dependency.VersionRange, hostVersion);
}
private InstalledManifest AddOrUpdatePackage(
@@ -509,7 +488,7 @@ private InstalledManifest AddOrUpdatePackage(
return new InstalledManifest([.. packages]);
}
- private static Dictionary CreateHostPackageVersions()
+ internal static IReadOnlyDictionary CreateHostPackageVersions()
{
var abstractionsAssembly = typeof(UserSettings).Assembly;
var informationalVersion = abstractionsAssembly
@@ -542,8 +521,9 @@ private InstalledPackageInfo[] GetCompatibilityAwarePackages(
=> packages
.Select(package => package with
{
- IsCompatible = package.HostMajorVersion is null
- || package.HostMajorVersion == this.hostMajorVersion,
+ IsCompatible = PluginCompatibility.IsHostMajorCompatible(
+ package.HostMajorVersion,
+ this.hostMajorVersion),
})
.ToArray();
@@ -716,21 +696,6 @@ private static void TryDeleteFile(string path)
}
}
- public override void Dispose()
- {
- if (Interlocked.Exchange(ref this.disposeState, 1) != 0)
- {
- return;
- }
-
- base.Dispose();
- if (this.ownsHttpClient)
- {
- this.httpClient.Dispose();
- }
- this.operationLock.Dispose();
- this.refreshLock.Dispose();
- }
}
/// NuGetパッケージ情報
diff --git a/WindowTranslator/Modules/PluginStore/NuGetProtocolPluginMetadataSource.cs b/WindowTranslator/Modules/PluginStore/NuGetProtocolPluginMetadataSource.cs
deleted file mode 100644
index 8ddd583e..00000000
--- a/WindowTranslator/Modules/PluginStore/NuGetProtocolPluginMetadataSource.cs
+++ /dev/null
@@ -1,122 +0,0 @@
-using NuGet.Common;
-using NuGet.Packaging;
-using NuGet.Packaging.Core;
-using NuGet.Protocol;
-using NuGet.Protocol.Core.Types;
-using NuGet.Versioning;
-
-namespace WindowTranslator.Modules.PluginStore;
-
-internal interface INuGetPluginMetadataSource
-{
- Task> SearchAsync(
- string tag,
- bool includePrerelease,
- CancellationToken cancellationToken);
-
- Task> GetPackageVersionsAsync(
- string packageId,
- CancellationToken cancellationToken);
-
- Task GetReadmeUrlAsync(
- string packageId,
- NuGetVersion version,
- CancellationToken cancellationToken);
-}
-
-internal sealed class NuGetProtocolPluginMetadataSource(string serviceIndexUrl) : INuGetPluginMetadataSource
-{
- private const int SearchResultLimit = 100;
-
- private readonly SourceRepository repository = Repository.Factory.GetCoreV3(serviceIndexUrl);
-
- public async Task> SearchAsync(
- string tag,
- bool includePrerelease,
- CancellationToken cancellationToken)
- {
- var searchResource = await this.repository
- .GetResourceAsync(cancellationToken)
- .ConfigureAwait(false);
- var searchFilter = new SearchFilter(includePrerelease)
- {
- IncludeDelisted = false,
- };
- var results = await searchResource.SearchAsync(
- $"tags:{tag}",
- searchFilter,
- skip: 0,
- take: SearchResultLimit,
- NullLogger.Instance,
- cancellationToken).ConfigureAwait(false);
-
- return results
- .Where(metadata => !string.IsNullOrWhiteSpace(metadata.Identity?.Id))
- .Select(metadata => new NuGetPluginSearchMetadata(
- metadata.Identity.Id,
- metadata.Title,
- metadata.Description,
- metadata.Authors,
- metadata.ProjectUrl?.AbsoluteUri,
- metadata.LicenseUrl?.AbsoluteUri))
- .ToArray();
- }
-
- public async Task> GetPackageVersionsAsync(
- string packageId,
- CancellationToken cancellationToken)
- {
- var metadataResource = await this.repository
- .GetResourceAsync(cancellationToken)
- .ConfigureAwait(false);
- using var cacheContext = new SourceCacheContext();
- var versions = await metadataResource.GetMetadataAsync(
- packageId,
- includePrerelease: true,
- includeUnlisted: false,
- cacheContext,
- NullLogger.Instance,
- cancellationToken).ConfigureAwait(false);
-
- return versions
- .Where(metadata => metadata.Identity?.Version is not null)
- .Select(metadata => new NuGetPluginVersionMetadata(
- metadata.Identity.Version,
- metadata.IsListed,
- metadata.DependencySets?.ToArray() ?? []))
- .ToArray();
- }
-
- public async Task GetReadmeUrlAsync(
- string packageId,
- NuGetVersion version,
- CancellationToken cancellationToken)
- {
- var metadataResource = await this.repository
- .GetResourceAsync(cancellationToken)
- .ConfigureAwait(false);
- using var cacheContext = new SourceCacheContext();
- var metadata = await metadataResource.GetMetadataAsync(
- new PackageIdentity(packageId, version),
- cacheContext,
- NullLogger.Instance,
- cancellationToken).ConfigureAwait(false);
-
- return string.IsNullOrWhiteSpace(metadata?.ReadmeFileUrl)
- ? null
- : metadata.ReadmeFileUrl;
- }
-}
-
-internal sealed record NuGetPluginSearchMetadata(
- string Id,
- string? Title,
- string? Description,
- string? Authors,
- string? ProjectUrl,
- string? LicenseUrl);
-
-internal sealed record NuGetPluginVersionMetadata(
- NuGetVersion Version,
- bool IsListed,
- IReadOnlyList DependencyGroups);
diff --git a/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs b/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs
new file mode 100644
index 00000000..f813c3a8
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs
@@ -0,0 +1,21 @@
+using NuGet.Versioning;
+
+namespace WindowTranslator.Modules.PluginStore;
+
+internal static class PluginCompatibility
+{
+#if DISABLE_PLUGIN_COMPATIBILITY_VALIDATION
+ internal static bool ValidationDisabled => true;
+#else
+ internal static bool ValidationDisabled => false;
+#endif
+
+ internal static bool IsVersionCompatible(VersionRange? requiredVersion, NuGetVersion? hostVersion)
+ => ValidationDisabled
+ || (hostVersion is not null && requiredVersion?.Satisfies(hostVersion) is not false);
+
+ internal static bool IsHostMajorCompatible(int? installedHostMajorVersion, int hostMajorVersion)
+ => ValidationDisabled
+ || installedHostMajorVersion is null
+ || installedHostMajorVersion == hostMajorVersion;
+}
diff --git a/WindowTranslator/Program.cs b/WindowTranslator/Program.cs
index be8d99ef..4d80eb6c 100644
--- a/WindowTranslator/Program.cs
+++ b/WindowTranslator/Program.cs
@@ -2,6 +2,8 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
+using System.Net;
+using System.Net.Http;
using System.Reflection;
using System.Windows;
using System.Windows.Markup;
@@ -12,6 +14,8 @@
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using NuGet.Protocol;
+using NuGet.Protocol.Core.Types;
using Octokit;
using Sentry.Extensions.Logging;
using Weikio.PluginFramework.Abstractions;
@@ -161,7 +165,21 @@
builder.Services.AddPresentation();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
-builder.Services.AddSingleton()
+builder.Services.AddHttpClient(NuGetPluginService.HttpClientName, client =>
+ client.Timeout = TimeSpan.FromSeconds(30))
+ .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
+ {
+ AutomaticDecompression = DecompressionMethods.All,
+ });
+builder.Services.AddSingleton(_ =>
+ NuGet.Protocol.Core.Types.Repository.Factory.GetCoreV3(NuGetPluginService.NuGetServiceIndexUrl));
+builder.Services.AddSingleton(sp => new NuGetPluginService(
+ sp.GetRequiredService>(),
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ userPluginsDir,
+ NuGetPluginService.CreateHostPackageVersions(),
+ AppInfo.Instance.Version.Major))
.AddHostedService(sp => sp.GetRequiredService());
builder.Services.AddTransient();
builder.Services.AddTransient, ConfigureUserSettings>();
diff --git a/WindowTranslator/WindowTranslator.csproj b/WindowTranslator/WindowTranslator.csproj
index dd54bcec..7cff9ac0 100644
--- a/WindowTranslator/WindowTranslator.csproj
+++ b/WindowTranslator/WindowTranslator.csproj
@@ -21,7 +21,7 @@
false
- $(DefineConstants);NO_MUTEX
+ $(DefineConstants);NO_MUTEX;DISABLE_PLUGIN_COMPATIBILITY_VALIDATION
@@ -44,6 +44,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
+
From f44c7380b2901e0ef3b2ea1b70875272dace0aa2 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Wed, 5 Aug 2026 00:21:55 +0900
Subject: [PATCH 15/43] =?UTF-8?q?NuGet=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E9=87=8D=E8=A4=87=E3=81=A8=E6=A4=9C=E7=B4=A2=E3=82=A8?=
=?UTF-8?q?=E3=83=A9=E3=83=BC=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../NuGetPluginServiceTests.cs | 177 ++++++++++++++++++
.../Modules/PluginStore/NuGetPluginCatalog.cs | 5 -
.../Modules/PluginStore/NuGetPluginService.cs | 51 ++++-
.../PluginStore/PrioritizedPluginCatalog.cs | 41 ++++
WindowTranslator/Program.cs | 16 +-
WindowTranslator/Properties/Resources.ar.resx | 6 +
WindowTranslator/Properties/Resources.cs.resx | 6 +
WindowTranslator/Properties/Resources.de.resx | 6 +
WindowTranslator/Properties/Resources.es.resx | 6 +
WindowTranslator/Properties/Resources.fa.resx | 6 +
.../Properties/Resources.fil.resx | 6 +
WindowTranslator/Properties/Resources.fr.resx | 6 +
WindowTranslator/Properties/Resources.hi.resx | 6 +
WindowTranslator/Properties/Resources.hu.resx | 6 +
WindowTranslator/Properties/Resources.id.resx | 6 +
WindowTranslator/Properties/Resources.ko.resx | 6 +
WindowTranslator/Properties/Resources.ms.resx | 6 +
WindowTranslator/Properties/Resources.pl.resx | 6 +
.../Properties/Resources.pt-BR.resx | 6 +
WindowTranslator/Properties/Resources.ru.resx | 6 +
WindowTranslator/Properties/Resources.th.resx | 6 +
WindowTranslator/Properties/Resources.tr.resx | 6 +
WindowTranslator/Properties/Resources.vi.resx | 6 +
.../Properties/Resources.zh-CN.resx | 6 +
.../Properties/Resources.zh-TW.resx | 6 +
25 files changed, 390 insertions(+), 20 deletions(-)
create mode 100644 WindowTranslator/Modules/PluginStore/PrioritizedPluginCatalog.cs
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index 5f1664bf..e1a99f9b 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -18,6 +18,7 @@
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
+using Weikio.PluginFramework.Abstractions;
using Weikio.PluginFramework.Catalogs;
using Weikio.PluginFramework.Context;
using WindowTranslator.Modules;
@@ -561,6 +562,90 @@ [new InstalledPackageInfo("Installed.Plugin", "1.2.3")])),
}
}
+ [Fact]
+ public async Task RefreshReportsMetadataFailuresAndKeepsSuccessfulPackages()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.SearchResults =
+ [
+ CreatePackageSearchMetadata(
+ "Available.Plugin",
+ null,
+ null,
+ null,
+ null,
+ null),
+ CreatePackageSearchMetadata(
+ "Unavailable.Plugin",
+ null,
+ null,
+ null,
+ null,
+ null),
+ ];
+ handler.AddMetadataVersions(
+ "Available.Plugin",
+ CreatePluginVersionMetadata("1.0.0"));
+ handler.AddMetadataException(
+ "Unavailable.Plugin",
+ new HttpRequestException("Metadata request failed."));
+ using var service = CreateService(handler, testDirectory);
+
+ await service.RefreshPackageInformationAsync();
+
+ Assert.Equal(
+ "Available.Plugin",
+ Assert.Single(service.PackageSnapshot.Packages).Id);
+ var error = Assert.IsType(service.PackageSnapshot.Error);
+ Assert.Contains(
+ error.InnerExceptions,
+ exception => exception.Message.Contains(
+ "Unavailable.Plugin",
+ StringComparison.Ordinal));
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task RefreshReportsAnErrorWhenEveryMetadataRequestFails()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.SearchResults =
+ [
+ CreatePackageSearchMetadata(
+ "Unavailable.Plugin",
+ null,
+ null,
+ null,
+ null,
+ null),
+ ];
+ handler.AddMetadataException(
+ "Unavailable.Plugin",
+ new HttpRequestException("Metadata request failed."));
+ using var service = CreateService(handler, testDirectory);
+
+ await service.RefreshPackageInformationAsync();
+
+ Assert.True(service.PackageSnapshot.IsInitialized);
+ Assert.Empty(service.PackageSnapshot.Packages);
+ Assert.NotNull(service.PackageSnapshot.Error);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
[Fact]
public async Task SearchReturnsReleaseAndPrereleaseVersions()
{
@@ -1182,6 +1267,32 @@ public void CatalogSynchronizationFollowsCompatibilityValidationSetting()
}
}
+ [Fact]
+ public async Task PrioritizedCatalogUsesNuGetPluginAndRemovesBundledDuplicate()
+ {
+ var nugetCatalog = new TestPluginCatalog(
+ typeof(NuGetPluginTypes.ReplacedPlugin),
+ typeof(NuGetPluginTypes.NuGetOnlyPlugin));
+ var bundledCatalog = new TestPluginCatalog(
+ typeof(BundledPluginTypes.ReplacedPlugin),
+ typeof(BundledPluginTypes.BundledOnlyPlugin));
+ var catalog = new PrioritizedPluginCatalog(nugetCatalog, bundledCatalog);
+
+ await catalog.Initialize();
+
+ Assert.True(catalog.IsInitialized);
+ Assert.Equal(
+ [
+ typeof(NuGetPluginTypes.ReplacedPlugin),
+ typeof(NuGetPluginTypes.NuGetOnlyPlugin),
+ typeof(BundledPluginTypes.BundledOnlyPlugin),
+ ],
+ catalog.GetPlugins().Select(plugin => plugin.Type));
+ Assert.Equal(
+ typeof(NuGetPluginTypes.ReplacedPlugin),
+ catalog.Get(nameof(NuGetPluginTypes.ReplacedPlugin), new Version(1, 0)).Type);
+ }
+
[Fact]
public async Task CatalogLoadsARealAssemblyFromAPackageSubdirectory()
{
@@ -1585,6 +1696,63 @@ public Task> GetVersionsAsync()
=> Task.FromResult>([]);
}
+ private sealed class TestPluginCatalog : IPluginCatalog
+ {
+ private readonly List plugins;
+
+ public TestPluginCatalog(params Type[] pluginTypes)
+ {
+ this.plugins = pluginTypes
+ .Select(type => new Plugin(
+ type.Assembly,
+ type,
+ type.Name,
+ new Version(1, 0),
+ this,
+ string.Empty,
+ string.Empty,
+ string.Empty,
+ []))
+ .ToList();
+ }
+
+ public bool IsInitialized { get; private set; }
+
+ public Task Initialize()
+ {
+ this.IsInitialized = true;
+ return Task.CompletedTask;
+ }
+
+ public List GetPlugins() => [.. this.plugins];
+
+ public Plugin Get(string name, Version version)
+ => this.plugins.FirstOrDefault(plugin =>
+ plugin.Name == name && plugin.Version == version)!;
+ }
+
+ private static class NuGetPluginTypes
+ {
+ public sealed class ReplacedPlugin
+ {
+ }
+
+ public sealed class NuGetOnlyPlugin
+ {
+ }
+ }
+
+ private static class BundledPluginTypes
+ {
+ public sealed class ReplacedPlugin
+ {
+ }
+
+ public sealed class BundledOnlyPlugin
+ {
+ }
+ }
+
private sealed class InMemoryHttpClientFactory(InMemoryNuGetHandler handler) : IHttpClientFactory
{
public HttpClient CreateClient(string name) => new(handler, disposeHandler: false);
@@ -1595,6 +1763,8 @@ private sealed class InMemoryNuGetHandler : HttpMessageHandler
private readonly Dictionary<(string Id, string Version), byte[]> packages = new();
private readonly Dictionary> metadataVersions =
new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary metadataExceptions =
+ new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary readmeUrls =
new(StringComparer.OrdinalIgnoreCase);
@@ -1614,6 +1784,9 @@ public void AddPackage(string id, string version, byte[] package)
public void AddMetadataVersions(string packageId, params TestPackageVersion[] packageVersions)
=> this.metadataVersions[packageId] = packageVersions;
+ public void AddMetadataException(string packageId, Exception exception)
+ => this.metadataExceptions[packageId] = exception;
+
public void AddReadmeUrl(string packageId, string version, string url)
=> this.readmeUrls[GetReadmeKey(packageId, NuGetVersion.Parse(version))] = url;
@@ -1711,6 +1884,10 @@ public override Task> GetMetadataAsync(
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
+ if (source.metadataExceptions.TryGetValue(packageId, out var exception))
+ {
+ return Task.FromException>(exception);
+ }
var versions = source.metadataVersions.TryGetValue(packageId, out var packageVersions)
? packageVersions
: [];
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index caad75ae..46fcd8cb 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -24,11 +24,6 @@ public sealed class NuGetPluginCatalog : IPluginCatalog
private readonly FolderPluginCatalogOptions options;
private CompositePluginCatalog innerCatalog = new();
- public NuGetPluginCatalog(string sourceDir, FolderPluginCatalogOptions options)
- : this(sourceDir, DefaultTempDir, AppInfo.Instance.Version.Major, options)
- {
- }
-
public NuGetPluginCatalog(
string sourceDir,
int hostMajorVersion,
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 8e3bd218..c15b7b52 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -95,7 +95,9 @@ internal async Task RefreshPackageInformationAsync(CancellationToken cancellatio
Exception? error = null;
try
{
- packages = await SearchPackagesAsync(cancellationToken).ConfigureAwait(false);
+ var searchResult = await SearchPackagesCoreAsync(cancellationToken).ConfigureAwait(false);
+ packages = searchResult.Packages;
+ error = searchResult.Error;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
@@ -134,6 +136,18 @@ internal async Task RefreshPackageInformationAsync(CancellationToken cancellatio
/// NuGetでWindowTranslatorプラグインを検索します。
///
public async Task> SearchPackagesAsync(CancellationToken cancellationToken = default)
+ {
+ var result = await SearchPackagesCoreAsync(cancellationToken).ConfigureAwait(false);
+ if (result.Error is not null)
+ {
+ throw result.Error;
+ }
+
+ return result.Packages;
+ }
+
+ private async Task SearchPackagesCoreAsync(
+ CancellationToken cancellationToken)
{
var searchResource = await this.repository
.GetResourceAsync(cancellationToken)
@@ -158,10 +172,11 @@ public async Task> SearchPackagesAsync(Cancellat
using var request = await requestGate.EnterAsync(cancellationToken);
try
{
- return await CreateCompatiblePackageInfoAsync(
+ var package = await CreateCompatiblePackageInfoAsync(
data,
metadataResource,
cancellationToken).ConfigureAwait(false);
+ return new PackageMetadataResult(package, null);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
@@ -169,20 +184,35 @@ public async Task> SearchPackagesAsync(Cancellat
}
catch (Exception ex)
{
+ var packageError = new InvalidOperationException(
+ $"NuGetパッケージ {data.Identity.Id} のメタデータを取得できませんでした。",
+ ex);
this.logger.LogWarning(
- ex,
+ packageError,
"NuGetパッケージのプラグイン互換性を確認できなかったため除外します: {PackageId}",
data.Identity.Id);
- return null;
+ return new PackageMetadataResult(null, packageError);
}
});
- var packages = await Task.WhenAll(packageTasks).ConfigureAwait(false);
- var compatiblePackages = packages.Where(package => package is not null).Select(package => package!).ToArray();
+ var results = await Task.WhenAll(packageTasks).ConfigureAwait(false);
+ var compatiblePackages = results
+ .Where(result => result.Package is not null)
+ .Select(result => result.Package!)
+ .ToArray();
+ var errors = results
+ .Where(result => result.Error is not null)
+ .Select(result => result.Error!)
+ .ToArray();
+ var error = errors.Length == 0
+ ? null
+ : new AggregateException(
+ "一部のNuGetパッケージ情報を取得できませんでした。",
+ errors);
this.logger.LogInformation(
"NuGet互換性確認完了: {Count}件のWindowTranslatorプラグインが見つかりました。",
compatiblePackages.Length);
- return compatiblePackages;
+ return new(compatiblePackages, error);
}
///
@@ -696,6 +726,13 @@ private static void TryDeleteFile(string path)
}
}
+ private sealed record PackageMetadataResult(
+ NuGetPackageInfo? Package,
+ Exception? Error);
+
+ private sealed record PackageSearchResult(
+ IReadOnlyList Packages,
+ Exception? Error);
}
/// NuGetパッケージ情報
diff --git a/WindowTranslator/Modules/PluginStore/PrioritizedPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/PrioritizedPluginCatalog.cs
new file mode 100644
index 00000000..ac228137
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/PrioritizedPluginCatalog.cs
@@ -0,0 +1,41 @@
+using Weikio.PluginFramework.Abstractions;
+
+namespace WindowTranslator.Modules.PluginStore;
+
+///
+/// 優先カタログのプラグインを先に返し、同じ型名のフォールバックプラグインを除外します。
+///
+internal sealed class PrioritizedPluginCatalog(
+ IPluginCatalog preferredCatalog,
+ IPluginCatalog fallbackCatalog) : IPluginCatalog
+{
+ private readonly IPluginCatalog preferredCatalog = preferredCatalog;
+ private readonly IPluginCatalog fallbackCatalog = fallbackCatalog;
+
+ ///
+ public bool IsInitialized
+ => this.preferredCatalog.IsInitialized && this.fallbackCatalog.IsInitialized;
+
+ ///
+ public async Task Initialize()
+ {
+ await this.preferredCatalog.Initialize().ConfigureAwait(false);
+ await this.fallbackCatalog.Initialize().ConfigureAwait(false);
+ }
+
+ ///
+ public List GetPlugins()
+ {
+ var typeNames = new HashSet(StringComparer.Ordinal);
+ return this.preferredCatalog
+ .GetPlugins()
+ .Concat(this.fallbackCatalog.GetPlugins())
+ .Where(plugin => typeNames.Add(plugin.Type.Name))
+ .ToList();
+ }
+
+ ///
+ public Plugin Get(string name, Version version)
+ => this.GetPlugins().FirstOrDefault(plugin =>
+ plugin.Name == name && plugin.Version == version)!;
+}
diff --git a/WindowTranslator/Program.cs b/WindowTranslator/Program.cs
index 4d80eb6c..03bd1c92 100644
--- a/WindowTranslator/Program.cs
+++ b/WindowTranslator/Program.cs
@@ -115,19 +115,19 @@
.AddPluginType()
.AddPluginType();
-var pluginFolderCatalog = new CompositePluginCatalog();
+var userPluginsDir = Path.Combine(PathUtility.UserDir, "plugins");
+IPluginCatalog pluginFolderCatalog = new NuGetPluginCatalog(
+ userPluginsDir,
+ AppInfo.Instance.Version.Major,
+ new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } });
var appPluginDir = @".\plugins";
if (Directory.Exists(appPluginDir))
{
- pluginFolderCatalog.AddCatalog(new FolderPluginCatalog(appPluginDir, options: new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } }));
+ pluginFolderCatalog = new PrioritizedPluginCatalog(
+ pluginFolderCatalog,
+ new FolderPluginCatalog(appPluginDir, options: new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } }));
}
-var userPluginsDir = Path.Combine(PathUtility.UserDir, "plugins");
-pluginFolderCatalog.AddCatalog(new NuGetPluginCatalog(
- userPluginsDir,
- AppInfo.Instance.Version.Major,
- new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } }));
-
builder.Services.AddPluginCatalog(pluginFolderCatalog);
builder.Configuration
.AddCommandLine(args)
diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx
index 1de815fd..979d39c9 100644
--- a/WindowTranslator/Properties/Resources.ar.resx
+++ b/WindowTranslator/Properties/Resources.ar.resx
@@ -495,6 +495,12 @@
فشل التثبيت
+
+ هذا المكون الإضافي غير متوافق مع الإصدار الرئيسي الحالي من WindowTranslator.
+
+
+ إعادة التشغيل الآن
+
يرجى إعادة تشغيل WindowTranslator لتطبيق تغييرات المكون الإضافي.
diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx
index cd8b7a45..2b2280c0 100644
--- a/WindowTranslator/Properties/Resources.cs.resx
+++ b/WindowTranslator/Properties/Resources.cs.resx
@@ -385,6 +385,12 @@ Monitory nejsou podporovány.
Instalace se nezdařila
+
+ Tento plugin není kompatibilní s aktuální hlavní verzí aplikace WindowTranslator.
+
+
+ Restartovat nyní
+
Restartujte WindowTranslator, aby se změny pluginu projevily.
diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx
index 410b25d5..6381cbf3 100644
--- a/WindowTranslator/Properties/Resources.de.resx
+++ b/WindowTranslator/Properties/Resources.de.resx
@@ -504,6 +504,12 @@ Monitore werden nicht unterstützt.
Installation fehlgeschlagen
+
+ Dieses Plugin ist mit der aktuellen Hauptversion von WindowTranslator nicht kompatibel.
+
+
+ Jetzt neu starten
+
Bitte starten Sie WindowTranslator neu, um Plugin-Änderungen anzuwenden.
diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx
index 1b36ad36..35b9debe 100644
--- a/WindowTranslator/Properties/Resources.es.resx
+++ b/WindowTranslator/Properties/Resources.es.resx
@@ -495,6 +495,12 @@
Error de instalación
+
+ Este plugin no es compatible con la versión principal actual de WindowTranslator.
+
+
+ Reiniciar ahora
+
Reinicie WindowTranslator para aplicar los cambios del plugin.
diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx
index 0be9545a..006c1798 100644
--- a/WindowTranslator/Properties/Resources.fa.resx
+++ b/WindowTranslator/Properties/Resources.fa.resx
@@ -489,6 +489,12 @@
نصب ناموفق بود
+
+ این افزونه با نسخه اصلی فعلی WindowTranslator سازگار نیست.
+
+
+ اکنون راهاندازی مجدد شود
+
لطفاً WindowTranslator را مجدداً راهاندازی کنید تا تغییرات افزونه اعمال شود.
diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx
index 5d860d57..a39ad460 100644
--- a/WindowTranslator/Properties/Resources.fil.resx
+++ b/WindowTranslator/Properties/Resources.fil.resx
@@ -504,6 +504,12 @@ Ang monitor ay hindi suportado.
Nabigo ang pag-install
+
+ Hindi tugma ang plugin na ito sa kasalukuyang pangunahing bersyon ng WindowTranslator.
+
+
+ I-restart ngayon
+
Mangyaring i-restart ang WindowTranslator upang mailapat ang mga pagbabago sa plugin.
diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx
index aac8ccbb..773a85ae 100644
--- a/WindowTranslator/Properties/Resources.fr.resx
+++ b/WindowTranslator/Properties/Resources.fr.resx
@@ -495,6 +495,12 @@
Échec de l'installation
+
+ Ce plugin n’est pas compatible avec la version majeure actuelle de WindowTranslator.
+
+
+ Redémarrer maintenant
+
Veuillez redémarrer WindowTranslator pour appliquer les modifications de plugin.
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index 469aa170..04c59816 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -497,6 +497,12 @@
इंस्टॉलेशन विफल
+
+ यह प्लगइन WindowTranslator के वर्तमान प्रमुख संस्करण के साथ संगत नहीं है।
+
+
+ अभी पुनः आरंभ करें
+
प्लगइन परिवर्तन लागू करने के लिए कृपया WindowTranslator को पुनः आरंभ करें।
diff --git a/WindowTranslator/Properties/Resources.hu.resx b/WindowTranslator/Properties/Resources.hu.resx
index dcc6720c..353f559e 100644
--- a/WindowTranslator/Properties/Resources.hu.resx
+++ b/WindowTranslator/Properties/Resources.hu.resx
@@ -385,6 +385,12 @@ A monitorok nem támogatottak.
A telepítés sikertelen
+
+ Ez a bővítmény nem kompatibilis a WindowTranslator jelenlegi főverziójával.
+
+
+ Újraindítás most
+
A bővítménymódosítások alkalmazásához indítsa újra a WindowTranslator alkalmazást.
diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx
index aa7140fc..3543c1e7 100644
--- a/WindowTranslator/Properties/Resources.id.resx
+++ b/WindowTranslator/Properties/Resources.id.resx
@@ -503,6 +503,12 @@ Monitor tidak didukung.
Instalasi gagal
+
+ Plugin ini tidak kompatibel dengan versi mayor WindowTranslator saat ini.
+
+
+ Mulai ulang sekarang
+
Silakan restart WindowTranslator untuk menerapkan perubahan plugin.
diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx
index 817dc5d9..d1825877 100644
--- a/WindowTranslator/Properties/Resources.ko.resx
+++ b/WindowTranslator/Properties/Resources.ko.resx
@@ -504,6 +504,12 @@
설치 실패
+
+ 이 플러그인은 현재 WindowTranslator 주 버전과 호환되지 않습니다.
+
+
+ 지금 다시 시작
+
플러그인 변경 사항을 적용하려면 WindowTranslator를 다시 시작하세요.
diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx
index 468b4d13..7e53277f 100644
--- a/WindowTranslator/Properties/Resources.ms.resx
+++ b/WindowTranslator/Properties/Resources.ms.resx
@@ -503,6 +503,12 @@ Monitor tidak disokong.
Pemasangan gagal
+
+ Pemalam ini tidak serasi dengan versi utama WindowTranslator semasa.
+
+
+ Mulakan semula sekarang
+
Sila mulakan semula WindowTranslator untuk menerapkan perubahan plugin.
diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx
index f667ae1c..f9bbba9a 100644
--- a/WindowTranslator/Properties/Resources.pl.resx
+++ b/WindowTranslator/Properties/Resources.pl.resx
@@ -504,6 +504,12 @@ Monitory nie są obsługiwane.
Instalacja nie powiodła się
+
+ Ta wtyczka nie jest zgodna z bieżącą główną wersją WindowTranslator.
+
+
+ Uruchom ponownie teraz
+
Uruchom ponownie WindowTranslator, aby zastosować zmiany wtyczki.
diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx
index 2fbacce7..4c0f7abc 100644
--- a/WindowTranslator/Properties/Resources.pt-BR.resx
+++ b/WindowTranslator/Properties/Resources.pt-BR.resx
@@ -503,6 +503,12 @@ Monitor tidak didukung.
Falha na instalação
+
+ Este plugin não é compatível com a versão principal atual do WindowTranslator.
+
+
+ Reiniciar agora
+
Reinicie o WindowTranslator para aplicar as alterações de plugin.
diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx
index 6fddcfb1..e9edbc8d 100644
--- a/WindowTranslator/Properties/Resources.ru.resx
+++ b/WindowTranslator/Properties/Resources.ru.resx
@@ -495,6 +495,12 @@
Ошибка установки
+
+ Этот плагин несовместим с текущей основной версией WindowTranslator.
+
+
+ Перезапустить сейчас
+
Перезапустите WindowTranslator для применения изменений плагина.
diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx
index 17bebcad..823db23f 100644
--- a/WindowTranslator/Properties/Resources.th.resx
+++ b/WindowTranslator/Properties/Resources.th.resx
@@ -504,6 +504,12 @@
การติดตั้งล้มเหลว
+
+ ปลั๊กอินนี้ไม่เข้ากันกับ WindowTranslator เวอร์ชันหลักปัจจุบัน
+
+
+ เริ่มใหม่ตอนนี้
+
กรุณาเริ่ม WindowTranslator ใหม่เพื่อนำการเปลี่ยนแปลงปลั๊กอินไปใช้
diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx
index 3a192391..54e3a7e6 100644
--- a/WindowTranslator/Properties/Resources.tr.resx
+++ b/WindowTranslator/Properties/Resources.tr.resx
@@ -504,6 +504,12 @@ Monitör desteklenmiyor.
Kurulum başarısız
+
+ Bu eklenti, WindowTranslator'ın mevcut ana sürümüyle uyumlu değil.
+
+
+ Şimdi yeniden başlat
+
Eklenti değişikliklerini uygulamak için lütfen WindowTranslator'ı yeniden başlatın.
diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx
index 878151c4..3c74da9e 100644
--- a/WindowTranslator/Properties/Resources.vi.resx
+++ b/WindowTranslator/Properties/Resources.vi.resx
@@ -504,6 +504,12 @@ Màn hình không được hỗ trợ.
Cài đặt thất bại
+
+ Plugin này không tương thích với phiên bản chính hiện tại của WindowTranslator.
+
+
+ Khởi động lại ngay
+
Vui lòng khởi động lại WindowTranslator để áp dụng thay đổi plugin.
diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx
index 04b14e05..672a4951 100644
--- a/WindowTranslator/Properties/Resources.zh-CN.resx
+++ b/WindowTranslator/Properties/Resources.zh-CN.resx
@@ -504,6 +504,12 @@
安装失败
+
+ 此插件与当前 WindowTranslator 主版本不兼容。
+
+
+ 立即重启
+
请重启 WindowTranslator 以应用插件更改。
diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx
index da6427bd..93a76e22 100644
--- a/WindowTranslator/Properties/Resources.zh-TW.resx
+++ b/WindowTranslator/Properties/Resources.zh-TW.resx
@@ -504,6 +504,12 @@
安裝失敗
+
+ 此外掛程式與目前的 WindowTranslator 主要版本不相容。
+
+
+ 立即重新啟動
+
請重新啟動 WindowTranslator 以套用外掛程式變更。
From 5cdcc954a7a7b25b276e86fec064779beb0fb752 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Wed, 5 Aug 2026 02:03:05 +0900
Subject: [PATCH 16/43] =?UTF-8?q?NuGet=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E7=AE=A1=E7=90=86=E9=A0=98=E5=9F=9F=E3=81=A8=E9=87=8D?=
=?UTF-8?q?=E8=A4=87=E5=87=A6=E7=90=86=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/dotnet-package.yml | 3 +-
.../NuGetPluginServiceTests.cs | 92 ++++++++++++-------
.../Modules/PluginStore/NuGetPluginCatalog.cs | 9 +-
.../Modules/PluginStore/NuGetPluginService.cs | 25 ++---
.../PluginStore/PrioritizedPluginCatalog.cs | 40 ++++++--
WindowTranslator/Program.cs | 18 +++-
docs/plugin.md | 5 +-
7 files changed, 131 insertions(+), 61 deletions(-)
diff --git a/.github/workflows/dotnet-package.yml b/.github/workflows/dotnet-package.yml
index d8c7082d..b2ad6bf6 100644
--- a/.github/workflows/dotnet-package.yml
+++ b/.github/workflows/dotnet-package.yml
@@ -70,7 +70,8 @@ jobs:
-p:Version=${{ steps.package-version.outputs.version }} `
-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
- -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
+ -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} `
+ -p:DecryptKey="${{ secrets.WINDOWTRANSLATOR_DECRYPTKEY }}"
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index e1a99f9b..b39baf52 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -3,6 +3,8 @@
using System.IO.Compression;
using System.Net;
using System.Net.Http;
+using System.Reflection;
+using System.Reflection.Emit;
using System.Resources;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
@@ -271,7 +273,7 @@ public async Task UninstallRestoresManagedFilesWhenManifestUpdateFails()
Assert.True(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
Assert.Empty(Directory.GetDirectories(
- testDirectory,
+ Path.Combine(testDirectory, NuGetPluginService.OperationsDirectoryName),
"Root.Plugin.uninstalling-*"));
var installed = Assert.Single(await service.GetInstalledPackagesAsync());
Assert.Equal("1.0.0", installed.Version);
@@ -1108,11 +1110,16 @@ public void CatalogSynchronizationCopiesOnlyChangesAndRemovesStaleFiles()
Path.Combine(sourceDirectory, "Root.Plugin", "Unchanged.dll");
File.WriteAllText(unchangedSourcePath, "unchanged");
Directory.CreateDirectory(Path.Combine(sourceDirectory, "Empty.Plugin"));
+ Directory.CreateDirectory(Path.Combine(sourceDirectory, ".operations"));
+ Directory.CreateDirectory(Path.Combine(sourceDirectory, ".operations", "backup-test"));
+ File.WriteAllText(
+ Path.Combine(sourceDirectory, ".operations", "backup-test", "old.dll"),
+ "old");
Directory.CreateDirectory(Path.Combine(sourceDirectory, "Root.Plugin.backup-test"));
File.WriteAllText(
Path.Combine(sourceDirectory, "Root.Plugin.backup-test", "old.dll"),
"old");
- Directory.CreateDirectory(Path.Combine(sourceDirectory, ".Root.Plugin.installing-test"));
+ Directory.CreateDirectory(Path.Combine(sourceDirectory, "Root.Plugin.installing-test"));
Directory.CreateDirectory(Path.Combine(sourceDirectory, "Root.Plugin.uninstalling-test"));
Directory.CreateDirectory(Path.Combine(destinationDirectory, "Root.Plugin"));
var destinationPluginPath =
@@ -1177,10 +1184,12 @@ public void CatalogSynchronizationCopiesOnlyChangesAndRemovesStaleFiles()
Assert.False(File.Exists(
Path.Combine(destinationDirectory, "nuget-manifest.json.tmp-test")));
Assert.False(Directory.Exists(
+ Path.Combine(destinationDirectory, ".operations")));
+ Assert.True(Directory.Exists(
Path.Combine(destinationDirectory, "Root.Plugin.backup-test")));
- Assert.False(Directory.Exists(
- Path.Combine(destinationDirectory, ".Root.Plugin.installing-test")));
- Assert.False(Directory.Exists(
+ Assert.True(Directory.Exists(
+ Path.Combine(destinationDirectory, "Root.Plugin.installing-test")));
+ Assert.True(Directory.Exists(
Path.Combine(destinationDirectory, "Root.Plugin.uninstalling-test")));
}
finally
@@ -1268,14 +1277,37 @@ public void CatalogSynchronizationFollowsCompatibilityValidationSetting()
}
[Fact]
- public async Task PrioritizedCatalogUsesNuGetPluginAndRemovesBundledDuplicate()
+ public async Task PrioritizedCatalogReplacesFallbackAssemblyAndKeepsSameTypeNameFromOtherAssembly()
{
+ var replacedAssemblyName = $"Replaced.Plugin.{Guid.NewGuid():N}";
+ var nugetTypes = CreatePluginTypes(
+ replacedAssemblyName,
+ "NuGet.ReplacedPlugin",
+ "NuGet.NuGetOnlyPlugin");
+ var replacedBundledTypes = CreatePluginTypes(
+ replacedAssemblyName,
+ "Bundled.ReplacedPlugin",
+ "Bundled.AlsoReplacedPlugin");
+ var duplicateNugetTypes = CreatePluginTypes(
+ replacedAssemblyName,
+ "DuplicateNuGet.ReplacedPlugin",
+ "DuplicateNuGet.AlsoReplacedPlugin");
+ var bundledOnlyType = CreatePluginTypes(
+ $"Bundled.Plugin.{Guid.NewGuid():N}",
+ "Bundled.BundledOnlyPlugin")[0];
+ var sameTypeNameFromOtherAssembly = CreatePluginTypes(
+ $"Other.Plugin.{Guid.NewGuid():N}",
+ "Other.ReplacedPlugin")[0];
var nugetCatalog = new TestPluginCatalog(
- typeof(NuGetPluginTypes.ReplacedPlugin),
- typeof(NuGetPluginTypes.NuGetOnlyPlugin));
+ nugetTypes[0],
+ nugetTypes[1],
+ duplicateNugetTypes[0],
+ duplicateNugetTypes[1]);
var bundledCatalog = new TestPluginCatalog(
- typeof(BundledPluginTypes.ReplacedPlugin),
- typeof(BundledPluginTypes.BundledOnlyPlugin));
+ replacedBundledTypes[0],
+ replacedBundledTypes[1],
+ bundledOnlyType,
+ sameTypeNameFromOtherAssembly);
var catalog = new PrioritizedPluginCatalog(nugetCatalog, bundledCatalog);
await catalog.Initialize();
@@ -1283,14 +1315,15 @@ public async Task PrioritizedCatalogUsesNuGetPluginAndRemovesBundledDuplicate()
Assert.True(catalog.IsInitialized);
Assert.Equal(
[
- typeof(NuGetPluginTypes.ReplacedPlugin),
- typeof(NuGetPluginTypes.NuGetOnlyPlugin),
- typeof(BundledPluginTypes.BundledOnlyPlugin),
+ nugetTypes[0],
+ nugetTypes[1],
+ bundledOnlyType,
+ sameTypeNameFromOtherAssembly,
],
catalog.GetPlugins().Select(plugin => plugin.Type));
Assert.Equal(
- typeof(NuGetPluginTypes.ReplacedPlugin),
- catalog.Get(nameof(NuGetPluginTypes.ReplacedPlugin), new Version(1, 0)).Type);
+ nugetTypes[0],
+ catalog.Get("ReplacedPlugin", new Version(1, 0)).Type);
}
[Fact]
@@ -1731,26 +1764,17 @@ public Plugin Get(string name, Version version)
plugin.Name == name && plugin.Version == version)!;
}
- private static class NuGetPluginTypes
+ private static Type[] CreatePluginTypes(string assemblyName, params string[] typeNames)
{
- public sealed class ReplacedPlugin
- {
- }
-
- public sealed class NuGetOnlyPlugin
- {
- }
- }
-
- private static class BundledPluginTypes
- {
- public sealed class ReplacedPlugin
- {
- }
-
- public sealed class BundledOnlyPlugin
- {
- }
+ var assembly = AssemblyBuilder.DefineDynamicAssembly(
+ new AssemblyName(assemblyName),
+ AssemblyBuilderAccess.Run);
+ var module = assembly.DefineDynamicModule(assemblyName);
+ return typeNames
+ .Select(typeName => module
+ .DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class)
+ .CreateType()!)
+ .ToArray();
}
private sealed class InMemoryHttpClientFactory(InMemoryNuGetHandler handler) : IHttpClientFactory
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 46fcd8cb..80880d89 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -16,7 +16,7 @@ namespace WindowTranslator.Modules.PluginStore;
public sealed class NuGetPluginCatalog : IPluginCatalog
{
private static readonly string DefaultTempDir =
- Path.Combine(Path.GetTempPath(), "WindowTranslator", "plugins");
+ Path.Combine(Path.GetTempPath(), "WindowTranslator", "nuget-plugins");
private readonly string sourceDir;
private readonly string tempDir;
@@ -376,10 +376,9 @@ internal static void SynchronizePluginFiles(
}
private static bool IsWorkingDirectory(string directoryName)
- => directoryName.EndsWith(".backup", StringComparison.OrdinalIgnoreCase)
- || directoryName.Contains(".backup-", StringComparison.OrdinalIgnoreCase)
- || directoryName.Contains(".uninstalling-", StringComparison.OrdinalIgnoreCase)
- || directoryName.Contains(".installing-", StringComparison.OrdinalIgnoreCase);
+ => directoryName.Equals(
+ NuGetPluginService.OperationsDirectoryName,
+ StringComparison.OrdinalIgnoreCase);
private static bool IsManagementFile(string fileName)
=> fileName.Equals("nuget-manifest.json", StringComparison.OrdinalIgnoreCase)
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index c15b7b52..2147b588 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -23,6 +23,7 @@ public sealed class NuGetPluginService : BackgroundService
internal const string HttpClientName = "NuGetPluginReadme";
internal const string PluginTag = "windowtranslator-plugin";
internal const string AbstractionsPackageId = "WindowTranslator.Abstractions";
+ internal const string OperationsDirectoryName = ".operations";
private const int SearchResultLimit = 100;
private const int MaxConcurrentMetadataRequests = 8;
private static readonly TimeSpan PackageInformationRefreshInterval = TimeSpan.FromHours(1);
@@ -38,7 +39,8 @@ public sealed class NuGetPluginService : BackgroundService
private readonly IHttpClientFactory httpClientFactory;
private readonly SourceRepository repository;
private readonly ILogger logger;
- private readonly string userPluginsDir;
+ private readonly string nugetPluginsDir;
+ private readonly string operationsDir;
private readonly string manifestPath;
private readonly IReadOnlyDictionary hostPackageVersions;
private readonly int hostMajorVersion;
@@ -52,15 +54,16 @@ internal NuGetPluginService(
ILogger logger,
IHttpClientFactory httpClientFactory,
SourceRepository repository,
- string userPluginsDir,
+ string nugetPluginsDir,
IReadOnlyDictionary hostPackageVersions,
int hostMajorVersion)
{
this.logger = logger;
this.httpClientFactory = httpClientFactory;
this.repository = repository;
- this.userPluginsDir = Path.GetFullPath(userPluginsDir);
- this.manifestPath = Path.Combine(this.userPluginsDir, "nuget-manifest.json");
+ this.nugetPluginsDir = Path.GetFullPath(nugetPluginsDir);
+ this.operationsDir = Path.Combine(this.nugetPluginsDir, OperationsDirectoryName);
+ this.manifestPath = Path.Combine(this.nugetPluginsDir, "nuget-manifest.json");
this.hostPackageVersions = hostPackageVersions;
this.hostMajorVersion = hostMajorVersion;
}
@@ -265,14 +268,14 @@ public async Task InstallPackageAsync(string packageId, string version, IProgres
{
var operationId = Guid.NewGuid().ToString("N");
var targetDir = GetPackageDirectory(packageId);
- var stagingDir = Path.Combine(this.userPluginsDir, $".{packageId}.installing-{operationId}");
- var backupDir = $"{targetDir}.backup-{operationId}";
+ var stagingDir = Path.Combine(this.operationsDir, $"{packageId}.installing-{operationId}");
+ var backupDir = Path.Combine(this.operationsDir, $"{packageId}.backup-{operationId}");
var targetMoved = false;
var stagingMoved = false;
using var operation = await this.operationLock.EnterAsync(cancellationToken);
try
{
- Directory.CreateDirectory(this.userPluginsDir);
+ Directory.CreateDirectory(this.operationsDir);
var packageResource = await this.repository
.GetResourceAsync(cancellationToken)
.ConfigureAwait(false);
@@ -355,11 +358,11 @@ public async Task UninstallPackageAsync(string packageId, CancellationToken canc
{
var operationId = Guid.NewGuid().ToString("N");
var targetDir = GetPackageDirectory(packageId);
- var uninstallingDir = $"{targetDir}.uninstalling-{operationId}";
+ var uninstallingDir = Path.Combine(this.operationsDir, $"{packageId}.uninstalling-{operationId}");
var targetMoved = false;
using var operation = await this.operationLock.EnterAsync(cancellationToken);
this.logger.LogInformation("パッケージをアンインストール: {PackageId}", packageId);
- Directory.CreateDirectory(this.userPluginsDir);
+ Directory.CreateDirectory(this.operationsDir);
var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
var updatedManifest = RemovePackage(manifest, packageId);
@@ -642,7 +645,7 @@ private async Task LoadManifestAsync(CancellationToken cancel
private async Task SaveManifestAsync(InstalledManifest manifest, CancellationToken cancellationToken)
{
- Directory.CreateDirectory(this.userPluginsDir);
+ Directory.CreateDirectory(this.nugetPluginsDir);
var temporaryPath = $"{this.manifestPath}.tmp-{Guid.NewGuid():N}";
try
{
@@ -684,7 +687,7 @@ private string GetPackageDirectory(string packageId)
throw new InvalidOperationException($"不正なNuGetパッケージIDです: {packageId}");
}
- var root = this.userPluginsDir
+ var root = this.nugetPluginsDir
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
var packageDirectory = Path.GetFullPath(Path.Combine(root, packageId));
diff --git a/WindowTranslator/Modules/PluginStore/PrioritizedPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/PrioritizedPluginCatalog.cs
index ac228137..30d8714f 100644
--- a/WindowTranslator/Modules/PluginStore/PrioritizedPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/PrioritizedPluginCatalog.cs
@@ -1,9 +1,10 @@
+using System.Reflection;
using Weikio.PluginFramework.Abstractions;
namespace WindowTranslator.Modules.PluginStore;
///
-/// 優先カタログのプラグインを先に返し、同じ型名のフォールバックプラグインを除外します。
+/// 優先カタログのプラグインを先に返し、同じアセンブリ名のフォールバックプラグインを除外します。
///
internal sealed class PrioritizedPluginCatalog(
IPluginCatalog preferredCatalog,
@@ -26,11 +27,38 @@ public async Task Initialize()
///
public List GetPlugins()
{
- var typeNames = new HashSet(StringComparer.Ordinal);
- return this.preferredCatalog
- .GetPlugins()
- .Concat(this.fallbackCatalog.GetPlugins())
- .Where(plugin => typeNames.Add(plugin.Type.Name))
+ var selectedAssemblyNames = new HashSet(StringComparer.OrdinalIgnoreCase);
+ return SelectPluginsByAssembly(
+ this.preferredCatalog.GetPlugins(),
+ selectedAssemblyNames)
+ .Concat(SelectPluginsByAssembly(
+ this.fallbackCatalog.GetPlugins(),
+ selectedAssemblyNames))
+ .ToList();
+ }
+
+ private static List SelectPluginsByAssembly(
+ List plugins,
+ HashSet selectedAssemblyNames)
+ {
+ var selectedAssemblies = new HashSet(ReferenceEqualityComparer.Instance);
+ foreach (var plugin in plugins)
+ {
+ var assembly = plugin.Type.Assembly;
+ if (selectedAssemblies.Contains(assembly))
+ {
+ continue;
+ }
+
+ var assemblyName = assembly.GetName().Name;
+ if (assemblyName is null || selectedAssemblyNames.Add(assemblyName))
+ {
+ selectedAssemblies.Add(assembly);
+ }
+ }
+
+ return plugins
+ .Where(plugin => selectedAssemblies.Contains(plugin.Type.Assembly))
.ToList();
}
diff --git a/WindowTranslator/Program.cs b/WindowTranslator/Program.cs
index 03bd1c92..addd1d80 100644
--- a/WindowTranslator/Program.cs
+++ b/WindowTranslator/Program.cs
@@ -116,16 +116,28 @@
.AddPluginType();
var userPluginsDir = Path.Combine(PathUtility.UserDir, "plugins");
+var nugetPluginsDir = Path.Combine(PathUtility.UserDir, "nuget-plugins");
IPluginCatalog pluginFolderCatalog = new NuGetPluginCatalog(
- userPluginsDir,
+ nugetPluginsDir,
AppInfo.Instance.Version.Major,
new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } });
+var fallbackPluginCatalogs = new List();
var appPluginDir = @".\plugins";
if (Directory.Exists(appPluginDir))
+{
+ fallbackPluginCatalogs.Add(
+ new FolderPluginCatalog(appPluginDir, options: new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } }));
+}
+if (Directory.Exists(userPluginsDir))
+{
+ fallbackPluginCatalogs.Add(
+ new FolderPluginCatalog(userPluginsDir, options: new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } }));
+}
+if (fallbackPluginCatalogs.Count > 0)
{
pluginFolderCatalog = new PrioritizedPluginCatalog(
pluginFolderCatalog,
- new FolderPluginCatalog(appPluginDir, options: new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } }));
+ new CompositePluginCatalog([.. fallbackPluginCatalogs]));
}
builder.Services.AddPluginCatalog(pluginFolderCatalog);
@@ -177,7 +189,7 @@
sp.GetRequiredService>(),
sp.GetRequiredService(),
sp.GetRequiredService(),
- userPluginsDir,
+ nugetPluginsDir,
NuGetPluginService.CreateHostPackageVersions(),
AppInfo.Instance.Version.Major))
.AddHostedService(sp => sp.GetRequiredService());
diff --git a/docs/plugin.md b/docs/plugin.md
index 7b59fcf3..8f695c9b 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -119,7 +119,10 @@ public class MyTranslateModule : ITranslateModule { ... }
インストールされたプラグインは以下のフォルダに配置されます:
-- Windows: `%USERPROFILE%\.wt\plugins\{PackageId}\`
+- NuGetからインストール: `%USERPROFILE%\.wt\nuget-plugins\{PackageId}\`
+- ユーザーが手動で配置: `%USERPROFILE%\.wt\plugins\`
+
+NuGetのインストール・更新・アンインストールでは、手動配置用フォルダの内容を変更しません。
## アプリからインストールする
From f0d1cd176db387fffaa4bb8785b7303b80642c75 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Thu, 6 Aug 2026 00:05:02 +0900
Subject: [PATCH 17/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E6=93=8D=E4=BD=9C=E3=81=AE=E4=B8=AD=E6=96=AD=E5=BE=A9?=
=?UTF-8?q?=E6=97=A7=E3=81=A8=E3=83=9E=E3=83=8B=E3=83=95=E3=82=A7=E3=82=B9?=
=?UTF-8?q?=E3=83=88=E6=A4=9C=E8=A8=BC=E3=82=92=E8=BF=BD=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../NuGetPluginServiceTests.cs | 162 ++++++++--
.../Modules/PluginStore/NuGetPluginCatalog.cs | 6 +-
.../PluginStore/NuGetPluginOperation.cs | 285 ++++++++++++++++++
.../Modules/PluginStore/NuGetPluginService.cs | 258 +++++++---------
.../PluginStore/PluginCompatibility.cs | 3 +-
5 files changed, 529 insertions(+), 185 deletions(-)
create mode 100644 WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index b39baf52..05e77327 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -382,32 +382,27 @@ await File.ReadAllTextAsync(
}
[Fact]
- public async Task ExistingManifestLoadsInstalledPackages()
+ public async Task ManifestLoadsInstalledPackages()
{
var testDirectory = CreateTestDirectory();
try
{
await File.WriteAllTextAsync(
Path.Combine(testDirectory, "nuget-manifest.json"),
- JsonSerializer.Serialize(new
- {
- Packages = new[]
- {
- new
- {
- Id = "Legacy.Plugin",
- Version = "1.0.0",
- },
- },
- }));
+ JsonSerializer.Serialize(new InstalledManifest(
+ [new InstalledPackageInfo("Root.Plugin", "1.0.0", HostMajorVersion: 7)])));
using var handler = new InMemoryNuGetHandler();
- using var service = CreateService(handler, testDirectory);
+ using var service = CreateService(
+ handler,
+ testDirectory,
+ hostMajorVersion: 7);
var installed = Assert.Single(await service.GetInstalledPackagesAsync());
- Assert.Equal("Legacy.Plugin", installed.Id);
+ Assert.Equal("Root.Plugin", installed.Id);
Assert.Equal("1.0.0", installed.Version);
+ Assert.Equal(7, installed.HostMajorVersion);
}
finally
{
@@ -416,33 +411,32 @@ await File.WriteAllTextAsync(
}
[Fact]
- public async Task ExistingManifestRecordsCurrentHostMajorVersionOnFirstRead()
+ public async Task ManifestWithoutHostMajorVersionIsRejected()
{
var testDirectory = CreateTestDirectory();
try
{
await File.WriteAllTextAsync(
Path.Combine(testDirectory, "nuget-manifest.json"),
- JsonSerializer.Serialize(new InstalledManifest(
- [new InstalledPackageInfo("Legacy.Plugin", "1.0.0")])));
+ JsonSerializer.Serialize(new
+ {
+ Packages = new[]
+ {
+ new
+ {
+ Id = "Legacy.Plugin",
+ Version = "1.0.0",
+ },
+ },
+ }));
using var handler = new InMemoryNuGetHandler();
using var service = CreateService(
handler,
testDirectory,
hostMajorVersion: 7);
- var package = Assert.Single(await service.GetInstalledPackagesAsync());
-
- Assert.Equal(7, package.HostMajorVersion);
- Assert.True(package.IsCompatible);
- using var document = JsonDocument.Parse(await File.ReadAllTextAsync(
- Path.Combine(testDirectory, "nuget-manifest.json")));
- Assert.Equal(
- 7,
- document.RootElement
- .GetProperty("Packages")[0]
- .GetProperty("HostMajorVersion")
- .GetInt32());
+ await Assert.ThrowsAsync(
+ () => service.GetInstalledPackagesAsync());
}
finally
{
@@ -537,13 +531,14 @@ public async Task PluginStoreKeepsInstalledPackagesVisibleWhenNuGetSearchFails()
await File.WriteAllTextAsync(
Path.Combine(testDirectory, "nuget-manifest.json"),
JsonSerializer.Serialize(new InstalledManifest(
- [new InstalledPackageInfo("Installed.Plugin", "1.2.3")])),
+ [new InstalledPackageInfo("Installed.Plugin", "1.2.3", HostMajorVersion: 7)])),
Encoding.UTF8);
using var handler = new InMemoryNuGetHandler();
handler.SearchException = new HttpRequestException("NuGet search failed.");
using var service = CreateService(
handler,
- testDirectory);
+ testDirectory,
+ hostMajorVersion: 7);
var viewModel = new PluginStoreViewModel(
service,
NullLogger.Instance,
@@ -1225,6 +1220,111 @@ public void CatalogSynchronizationClearsStaleFilesWhenSourceIsMissing()
}
}
+ [Fact]
+ public async Task InterruptedInstallIsRolledBackBeforeItIsTreatedAsCompleted()
+ {
+ var sourceDirectory = CreateTestDirectory();
+ try
+ {
+ const string packageId = "Root.Plugin";
+ var originalManifest = new InstalledManifest(
+ [new InstalledPackageInfo(packageId, "1.0.0", HostMajorVersion: 1)]);
+ var updatedManifest = new InstalledManifest(
+ [new InstalledPackageInfo(packageId, "2.0.0", HostMajorVersion: 1)]);
+ var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
+ await NuGetPluginOperation.SaveManifestAsync(
+ manifestPath,
+ updatedManifest,
+ CancellationToken.None);
+
+ var operationPaths = NuGetPluginOperation.CreatePaths(sourceDirectory, packageId);
+ Directory.CreateDirectory(operationPaths.BackupPath);
+ File.WriteAllText(Path.Combine(operationPaths.BackupPath, "plugin.txt"), "old");
+ var targetDirectory = NuGetPluginOperation.GetPackageDirectory(sourceDirectory, packageId);
+ Directory.CreateDirectory(targetDirectory);
+ File.WriteAllText(Path.Combine(targetDirectory, "plugin.txt"), "new");
+ await NuGetPluginOperation.WriteJournalAsync(
+ operationPaths,
+ new(
+ operationPaths.OperationId,
+ packageId,
+ NuGetPluginOperationKind.Install,
+ ManifestExisted: true,
+ originalManifest),
+ CancellationToken.None);
+
+ var unresolved = await NuGetPluginOperation.RecoverInterruptedOperationsAsync(
+ sourceDirectory);
+
+ Assert.Empty(unresolved);
+ Assert.Equal("old", File.ReadAllText(Path.Combine(targetDirectory, "plugin.txt")));
+ var restoredManifest = JsonSerializer.Deserialize(
+ File.ReadAllText(manifestPath),
+ NuGetPluginService.ManifestJsonOptions);
+ Assert.Equal("1.0.0", Assert.Single(restoredManifest!.Packages).Version);
+ Assert.False(File.Exists(operationPaths.JournalPath));
+ Assert.False(Directory.Exists(operationPaths.StagingPath));
+ Assert.False(Directory.Exists(operationPaths.BackupPath));
+ }
+ finally
+ {
+ DeleteTestDirectory(sourceDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task CompletedInstallKeepsNewFilesAndOnlyCleansOperationData()
+ {
+ var sourceDirectory = CreateTestDirectory();
+ try
+ {
+ const string packageId = "Root.Plugin";
+ var originalManifest = new InstalledManifest(
+ [new InstalledPackageInfo(packageId, "1.0.0", HostMajorVersion: 1)]);
+ var updatedManifest = new InstalledManifest(
+ [new InstalledPackageInfo(packageId, "2.0.0", HostMajorVersion: 1)]);
+ var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
+ await NuGetPluginOperation.SaveManifestAsync(
+ manifestPath,
+ updatedManifest,
+ CancellationToken.None);
+
+ var operationPaths = NuGetPluginOperation.CreatePaths(sourceDirectory, packageId);
+ Directory.CreateDirectory(operationPaths.BackupPath);
+ File.WriteAllText(Path.Combine(operationPaths.BackupPath, "plugin.txt"), "old");
+ var targetDirectory = NuGetPluginOperation.GetPackageDirectory(sourceDirectory, packageId);
+ Directory.CreateDirectory(targetDirectory);
+ File.WriteAllText(Path.Combine(targetDirectory, "plugin.txt"), "new");
+ await NuGetPluginOperation.WriteJournalAsync(
+ operationPaths,
+ new(
+ operationPaths.OperationId,
+ packageId,
+ NuGetPluginOperationKind.Install,
+ ManifestExisted: true,
+ originalManifest),
+ CancellationToken.None);
+ NuGetPluginOperation.MarkCommitted(operationPaths);
+
+ var unresolved = await NuGetPluginOperation.RecoverInterruptedOperationsAsync(
+ sourceDirectory);
+
+ Assert.Empty(unresolved);
+ Assert.Equal("new", File.ReadAllText(Path.Combine(targetDirectory, "plugin.txt")));
+ var retainedManifest = JsonSerializer.Deserialize(
+ File.ReadAllText(manifestPath),
+ NuGetPluginService.ManifestJsonOptions);
+ Assert.Equal("2.0.0", Assert.Single(retainedManifest!.Packages).Version);
+ Assert.False(File.Exists(operationPaths.JournalPath));
+ Assert.False(File.Exists(operationPaths.CommittedPath));
+ Assert.False(Directory.Exists(operationPaths.BackupPath));
+ }
+ finally
+ {
+ DeleteTestDirectory(sourceDirectory);
+ }
+ }
+
[Fact]
public void CatalogSynchronizationFollowsCompatibilityValidationSetting()
{
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 80880d89..6d709781 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -55,9 +55,13 @@ internal NuGetPluginCatalog(
///
public async Task Initialize()
{
+ var unresolvedOperations = await NuGetPluginOperation
+ .RecoverInterruptedOperationsAsync(this.sourceDir)
+ .ConfigureAwait(false);
var incompatiblePackages = GetIncompatiblePackageIds(
this.sourceDir,
- this.hostMajorVersion);
+ this.hostMajorVersion).ToHashSet(StringComparer.OrdinalIgnoreCase);
+ incompatiblePackages.UnionWith(unresolvedOperations);
SynchronizePluginFiles(this.sourceDir, this.tempDir, incompatiblePackages);
this.innerCatalog = CreateCatalog(this.tempDir, this.options);
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs
new file mode 100644
index 00000000..4292c752
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs
@@ -0,0 +1,285 @@
+using System.Diagnostics;
+using System.IO;
+using System.Text.Json;
+
+namespace WindowTranslator.Modules.PluginStore;
+
+internal enum NuGetPluginOperationKind
+{
+ Install,
+ Uninstall,
+}
+
+internal sealed record NuGetPluginOperationState(
+ string OperationId,
+ string PackageId,
+ NuGetPluginOperationKind Kind,
+ bool ManifestExisted,
+ InstalledManifest OriginalManifest);
+
+internal sealed record NuGetPluginOperationPaths(
+ string OperationId,
+ string PackageId,
+ string JournalPath,
+ string CommittedPath,
+ string StagingPath,
+ string BackupPath,
+ string UninstallingPath);
+
+internal static class NuGetPluginOperation
+{
+ private const string JournalSuffix = ".operation.json";
+ private const string CommittedSuffix = ".committed";
+
+ internal static NuGetPluginOperationPaths CreatePaths(string nugetPluginsDir, string packageId)
+ => GetPaths(nugetPluginsDir, packageId, Guid.NewGuid().ToString("N"));
+
+ internal static string GetPackageDirectory(string nugetPluginsDir, string packageId)
+ {
+ if (string.IsNullOrWhiteSpace(packageId)
+ || packageId is "." or ".."
+ || packageId.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0
+ || packageId.Contains(Path.DirectorySeparatorChar)
+ || packageId.Contains(Path.AltDirectorySeparatorChar))
+ {
+ throw new InvalidOperationException($"不正なNuGetパッケージIDです: {packageId}");
+ }
+
+ var root = Path.GetFullPath(nugetPluginsDir)
+ .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ + Path.DirectorySeparatorChar;
+ var packageDirectory = Path.GetFullPath(Path.Combine(root, packageId));
+ if (!packageDirectory.StartsWith(root, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException($"不正なNuGetパッケージIDです: {packageId}");
+ }
+
+ return packageDirectory;
+ }
+
+ internal static Task WriteJournalAsync(
+ NuGetPluginOperationPaths paths,
+ NuGetPluginOperationState state,
+ CancellationToken cancellationToken)
+ {
+ if (!paths.OperationId.Equals(state.OperationId, StringComparison.Ordinal)
+ || !paths.PackageId.Equals(state.PackageId, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException("NuGetプラグイン操作とジャーナルの対象が一致しません。");
+ }
+
+ return SaveJsonAsync(paths.JournalPath, state, cancellationToken);
+ }
+
+ internal static void MarkCommitted(NuGetPluginOperationPaths paths)
+ {
+ using var stream = new FileStream(
+ paths.CommittedPath,
+ FileMode.CreateNew,
+ FileAccess.Write,
+ FileShare.None,
+ bufferSize: 1,
+ FileOptions.WriteThrough);
+ stream.Flush(flushToDisk: true);
+ }
+
+ internal static async Task> RecoverInterruptedOperationsAsync(
+ string nugetPluginsDir,
+ CancellationToken cancellationToken = default)
+ {
+ var operationsDir = Path.Combine(
+ Path.GetFullPath(nugetPluginsDir),
+ NuGetPluginService.OperationsDirectoryName);
+ if (!Directory.Exists(operationsDir))
+ {
+ return new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+
+ var unresolvedPackageIds = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var journalPath in Directory.EnumerateFiles(
+ operationsDir,
+ $"*{JournalSuffix}",
+ SearchOption.TopDirectoryOnly))
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ NuGetPluginOperationState? state = null;
+ try
+ {
+ await using (var stream = File.OpenRead(journalPath))
+ {
+ state = await JsonSerializer.DeserializeAsync(
+ stream,
+ NuGetPluginService.ManifestJsonOptions,
+ cancellationToken).ConfigureAwait(false)
+ ?? throw new InvalidDataException("NuGetプラグイン操作ジャーナルが空です。");
+ }
+ var paths = GetPaths(nugetPluginsDir, state.PackageId, state.OperationId);
+ if (!paths.JournalPath.Equals(journalPath, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidDataException("NuGetプラグイン操作ジャーナルのIDが一致しません。");
+ }
+
+ if (File.Exists(paths.CommittedPath))
+ {
+ CleanupCommitted(paths);
+ continue;
+ }
+
+ await RollbackAsync(
+ nugetPluginsDir,
+ state,
+ paths,
+ cancellationToken).ConfigureAwait(false);
+ CleanupRolledBack(paths);
+ }
+ catch (Exception ex) when (ex is not OperationCanceledException)
+ {
+ if (!string.IsNullOrWhiteSpace(state?.PackageId))
+ {
+ unresolvedPackageIds.Add(state.PackageId);
+ }
+ Trace.TraceWarning(
+ "NuGetプラグイン操作の復旧に失敗しました: {0} ({1})",
+ journalPath,
+ ex);
+ }
+ }
+
+ return unresolvedPackageIds;
+ }
+
+ internal static async Task RollbackAsync(
+ string nugetPluginsDir,
+ NuGetPluginOperationState state,
+ NuGetPluginOperationPaths paths,
+ CancellationToken cancellationToken)
+ {
+ var targetDir = GetPackageDirectory(nugetPluginsDir, state.PackageId);
+ switch (state.Kind)
+ {
+ case NuGetPluginOperationKind.Install:
+ if (!Directory.Exists(paths.StagingPath) && Directory.Exists(targetDir))
+ {
+ Directory.Move(targetDir, paths.StagingPath);
+ }
+ if (Directory.Exists(paths.BackupPath))
+ {
+ Directory.Move(paths.BackupPath, targetDir);
+ }
+ break;
+ case NuGetPluginOperationKind.Uninstall:
+ if (Directory.Exists(paths.UninstallingPath) && !Directory.Exists(targetDir))
+ {
+ Directory.Move(paths.UninstallingPath, targetDir);
+ }
+ break;
+ default:
+ throw new InvalidDataException($"不明なNuGetプラグイン操作です: {state.Kind}");
+ }
+
+ var manifestPath = Path.Combine(Path.GetFullPath(nugetPluginsDir), "nuget-manifest.json");
+ if (state.ManifestExisted)
+ {
+ await SaveManifestAsync(
+ manifestPath,
+ state.OriginalManifest,
+ cancellationToken).ConfigureAwait(false);
+ }
+ else if (File.Exists(manifestPath))
+ {
+ File.Delete(manifestPath);
+ }
+ }
+
+ internal static void CleanupCommitted(NuGetPluginOperationPaths paths)
+ {
+ DeleteDirectoryIfExists(paths.StagingPath);
+ DeleteDirectoryIfExists(paths.BackupPath);
+ DeleteDirectoryIfExists(paths.UninstallingPath);
+ File.Delete(paths.JournalPath);
+ File.Delete(paths.CommittedPath);
+ }
+
+ internal static void CleanupRolledBack(NuGetPluginOperationPaths paths)
+ {
+ File.Delete(paths.JournalPath);
+ DeleteDirectoryIfExists(paths.StagingPath);
+ DeleteDirectoryIfExists(paths.BackupPath);
+ DeleteDirectoryIfExists(paths.UninstallingPath);
+ File.Delete(paths.CommittedPath);
+ }
+
+ internal static Task SaveManifestAsync(
+ string manifestPath,
+ InstalledManifest manifest,
+ CancellationToken cancellationToken)
+ => SaveJsonAsync(manifestPath, manifest, cancellationToken);
+
+ private static NuGetPluginOperationPaths GetPaths(
+ string nugetPluginsDir,
+ string packageId,
+ string operationId)
+ {
+ if (!Guid.TryParseExact(operationId, "N", out _))
+ {
+ throw new InvalidOperationException($"不正なNuGetプラグイン操作IDです: {operationId}");
+ }
+ _ = GetPackageDirectory(nugetPluginsDir, packageId);
+
+ var operationsDir = Path.Combine(
+ Path.GetFullPath(nugetPluginsDir),
+ NuGetPluginService.OperationsDirectoryName);
+ return new(
+ operationId,
+ packageId,
+ Path.Combine(operationsDir, $"{operationId}{JournalSuffix}"),
+ Path.Combine(operationsDir, $"{operationId}{CommittedSuffix}"),
+ Path.Combine(operationsDir, $"{packageId}.installing-{operationId}"),
+ Path.Combine(operationsDir, $"{packageId}.backup-{operationId}"),
+ Path.Combine(operationsDir, $"{packageId}.uninstalling-{operationId}"));
+ }
+
+ private static async Task SaveJsonAsync(
+ string destinationPath,
+ T value,
+ CancellationToken cancellationToken)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
+ var temporaryPath = $"{destinationPath}.tmp-{Guid.NewGuid():N}";
+ try
+ {
+ await using (var stream = new FileStream(
+ temporaryPath,
+ FileMode.CreateNew,
+ FileAccess.Write,
+ FileShare.None,
+ bufferSize: 4096,
+ useAsync: true))
+ {
+ await JsonSerializer.SerializeAsync(
+ stream,
+ value,
+ NuGetPluginService.ManifestJsonOptions,
+ cancellationToken).ConfigureAwait(false);
+ await stream.FlushAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ File.Move(temporaryPath, destinationPath, overwrite: true);
+ }
+ finally
+ {
+ if (File.Exists(temporaryPath))
+ {
+ File.Delete(temporaryPath);
+ }
+ }
+ }
+
+ private static void DeleteDirectoryIfExists(string path)
+ {
+ if (Directory.Exists(path))
+ {
+ Directory.Delete(path, recursive: true);
+ }
+ }
+}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 2147b588..218841e4 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -40,7 +40,6 @@ public sealed class NuGetPluginService : BackgroundService
private readonly SourceRepository repository;
private readonly ILogger logger;
private readonly string nugetPluginsDir;
- private readonly string operationsDir;
private readonly string manifestPath;
private readonly IReadOnlyDictionary hostPackageVersions;
private readonly int hostMajorVersion;
@@ -62,7 +61,6 @@ internal NuGetPluginService(
this.httpClientFactory = httpClientFactory;
this.repository = repository;
this.nugetPluginsDir = Path.GetFullPath(nugetPluginsDir);
- this.operationsDir = Path.Combine(this.nugetPluginsDir, OperationsDirectoryName);
this.manifestPath = Path.Combine(this.nugetPluginsDir, "nuget-manifest.json");
this.hostPackageVersions = hostPackageVersions;
this.hostMajorVersion = hostMajorVersion;
@@ -266,16 +264,14 @@ private async Task SearchPackagesCoreAsync(
///
public async Task InstallPackageAsync(string packageId, string version, IProgress? progress = null, CancellationToken cancellationToken = default)
{
- var operationId = Guid.NewGuid().ToString("N");
+ var operationPaths = NuGetPluginOperation.CreatePaths(this.nugetPluginsDir, packageId);
var targetDir = GetPackageDirectory(packageId);
- var stagingDir = Path.Combine(this.operationsDir, $"{packageId}.installing-{operationId}");
- var backupDir = Path.Combine(this.operationsDir, $"{packageId}.backup-{operationId}");
- var targetMoved = false;
- var stagingMoved = false;
+ NuGetPluginOperationState? operationState = null;
+ var journalWritten = false;
+ var committed = false;
using var operation = await this.operationLock.EnterAsync(cancellationToken);
try
{
- Directory.CreateDirectory(this.operationsDir);
var packageResource = await this.repository
.GetResourceAsync(cancellationToken)
.ConfigureAwait(false);
@@ -286,34 +282,48 @@ public async Task InstallPackageAsync(string packageId, string version, IProgres
await installer.InstallAsync(
packageId,
version,
- stagingDir,
+ operationPaths.StagingPath,
progress,
cancellationToken).ConfigureAwait(false);
+ var manifestExisted = File.Exists(this.manifestPath);
var currentManifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
var updatedManifest = AddOrUpdatePackage(currentManifest, packageId, version);
+ operationState = new(
+ operationPaths.OperationId,
+ packageId,
+ NuGetPluginOperationKind.Install,
+ manifestExisted,
+ currentManifest);
+ await NuGetPluginOperation.WriteJournalAsync(
+ operationPaths,
+ operationState,
+ cancellationToken).ConfigureAwait(false);
+ journalWritten = true;
if (Directory.Exists(targetDir))
{
- Directory.Move(targetDir, backupDir);
- targetMoved = true;
+ Directory.Move(targetDir, operationPaths.BackupPath);
}
- Directory.Move(stagingDir, targetDir);
- stagingMoved = true;
+ Directory.Move(operationPaths.StagingPath, targetDir);
await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
+ NuGetPluginOperation.MarkCommitted(operationPaths);
+ committed = true;
UpdateInstalledPackages(updatedManifest.Packages);
try
{
- if (Directory.Exists(backupDir))
- {
- Directory.Delete(backupDir, recursive: true);
- }
+ NuGetPluginOperation.CleanupCommitted(operationPaths);
+ journalWritten = false;
}
catch (Exception ex)
{
- this.logger.LogWarning(ex, "プラグインバックアップの削除に失敗しました: {BackupDir}", backupDir);
+ this.logger.LogWarning(
+ ex,
+ "完了したプラグイン操作の後片付けに失敗しました: {PackageId} {OperationId}",
+ packageId,
+ operationPaths.OperationId);
}
this.logger.LogInformation(
@@ -324,29 +334,34 @@ await installer.InstallAsync(
}
catch
{
- try
+ if (journalWritten && !committed && operationState is not null)
{
- if (stagingMoved && Directory.Exists(targetDir))
+ try
{
- Directory.Move(targetDir, stagingDir);
+ await NuGetPluginOperation.RollbackAsync(
+ this.nugetPluginsDir,
+ operationState,
+ operationPaths,
+ CancellationToken.None).ConfigureAwait(false);
+ NuGetPluginOperation.CleanupRolledBack(operationPaths);
+ journalWritten = false;
}
- if (targetMoved && Directory.Exists(backupDir))
+ catch (Exception rollbackException)
{
- Directory.Move(backupDir, targetDir);
+ this.logger.LogError(
+ rollbackException,
+ "プラグイン {PackageId} のインストール失敗後の復旧に失敗しました。",
+ packageId);
}
}
- catch (Exception rollbackException)
- {
- this.logger.LogError(
- rollbackException,
- "プラグイン {PackageId} のインストール失敗後の復旧に失敗しました。",
- packageId);
- }
throw;
}
finally
{
- TryDeleteDirectory(stagingDir);
+ if (!journalWritten)
+ {
+ TryDeleteDirectory(operationPaths.StagingPath);
+ }
}
}
@@ -356,62 +371,77 @@ await installer.InstallAsync(
///
public async Task UninstallPackageAsync(string packageId, CancellationToken cancellationToken = default)
{
- var operationId = Guid.NewGuid().ToString("N");
+ var operationPaths = NuGetPluginOperation.CreatePaths(this.nugetPluginsDir, packageId);
var targetDir = GetPackageDirectory(packageId);
- var uninstallingDir = Path.Combine(this.operationsDir, $"{packageId}.uninstalling-{operationId}");
- var targetMoved = false;
+ NuGetPluginOperationState? operationState = null;
+ var journalWritten = false;
+ var committed = false;
using var operation = await this.operationLock.EnterAsync(cancellationToken);
this.logger.LogInformation("パッケージをアンインストール: {PackageId}", packageId);
- Directory.CreateDirectory(this.operationsDir);
-
- var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
- var updatedManifest = RemovePackage(manifest, packageId);
-
- if (Directory.Exists(targetDir))
- {
- Directory.Move(targetDir, uninstallingDir);
- targetMoved = true;
- }
try
{
+ var manifestExisted = File.Exists(this.manifestPath);
+ var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ var updatedManifest = RemovePackage(manifest, packageId);
+ operationState = new(
+ operationPaths.OperationId,
+ packageId,
+ NuGetPluginOperationKind.Uninstall,
+ manifestExisted,
+ manifest);
+ await NuGetPluginOperation.WriteJournalAsync(
+ operationPaths,
+ operationState,
+ cancellationToken).ConfigureAwait(false);
+ journalWritten = true;
+
+ if (Directory.Exists(targetDir))
+ {
+ Directory.Move(targetDir, operationPaths.UninstallingPath);
+ }
+
await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
+ NuGetPluginOperation.MarkCommitted(operationPaths);
+ committed = true;
UpdateInstalledPackages(updatedManifest.Packages);
- }
- catch
- {
+
try
{
- if (targetMoved
- && Directory.Exists(uninstallingDir)
- && !Directory.Exists(targetDir))
- {
- Directory.Move(uninstallingDir, targetDir);
- }
+ NuGetPluginOperation.CleanupCommitted(operationPaths);
+ journalWritten = false;
}
- catch (Exception rollbackException)
+ catch (Exception ex)
{
- this.logger.LogError(
- rollbackException,
- "プラグイン {PackageId} のアンインストール失敗後の復旧に失敗しました。",
- packageId);
+ this.logger.LogWarning(
+ ex,
+ "完了したプラグイン操作の後片付けに失敗しました: {PackageId} {OperationId}",
+ packageId,
+ operationPaths.OperationId);
}
- throw;
}
-
- try
+ catch
{
- if (Directory.Exists(uninstallingDir))
+ if (journalWritten && !committed && operationState is not null)
{
- Directory.Delete(uninstallingDir, recursive: true);
+ try
+ {
+ await NuGetPluginOperation.RollbackAsync(
+ this.nugetPluginsDir,
+ operationState,
+ operationPaths,
+ CancellationToken.None).ConfigureAwait(false);
+ NuGetPluginOperation.CleanupRolledBack(operationPaths);
+ }
+ catch (Exception rollbackException)
+ {
+ this.logger.LogError(
+ rollbackException,
+ "プラグイン {PackageId} のアンインストール失敗後の復旧に失敗しました。",
+ packageId);
+ }
}
- }
- catch (Exception ex)
- {
- this.logger.LogWarning(
- ex,
- "アンインストール済みプラグインフォルダの削除に失敗しました: {Directory}",
- uninstallingDir);
+ throw;
}
this.logger.LogInformation(
@@ -426,18 +456,6 @@ public async Task> GetInstalledPackagesAsync
{
using var operation = await this.operationLock.EnterAsync(cancellationToken);
var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
- var migratedPackages = manifest.Packages
- .Select(package => package.HostMajorVersion is null
- ? package with { HostMajorVersion = this.hostMajorVersion }
- : package)
- .ToList();
- if (migratedPackages.Where((package, index) =>
- package != manifest.Packages[index]).Any())
- {
- manifest = new InstalledManifest(migratedPackages);
- await SaveManifestAsync(manifest, cancellationToken).ConfigureAwait(false);
- }
-
return GetCompatibilityAwarePackages(manifest.Packages);
}
@@ -643,61 +661,14 @@ private async Task LoadManifestAsync(CancellationToken cancel
}
}
- private async Task SaveManifestAsync(InstalledManifest manifest, CancellationToken cancellationToken)
- {
- Directory.CreateDirectory(this.nugetPluginsDir);
- var temporaryPath = $"{this.manifestPath}.tmp-{Guid.NewGuid():N}";
- try
- {
- await using (var fs = new FileStream(
- temporaryPath,
- FileMode.CreateNew,
- FileAccess.Write,
- FileShare.None,
- bufferSize: 4096,
- useAsync: true))
- {
- await JsonSerializer.SerializeAsync(
- fs,
- manifest,
- ManifestJsonOptions,
- cancellationToken).ConfigureAwait(false);
- await fs.FlushAsync(cancellationToken).ConfigureAwait(false);
- }
-
- ReplaceFile(temporaryPath, this.manifestPath);
- }
- finally
- {
- TryDeleteFile(temporaryPath);
- }
- }
-
- private static void ReplaceFile(string sourcePath, string destinationPath)
- => File.Move(sourcePath, destinationPath, overwrite: true);
+ private Task SaveManifestAsync(InstalledManifest manifest, CancellationToken cancellationToken)
+ => NuGetPluginOperation.SaveManifestAsync(
+ this.manifestPath,
+ manifest,
+ cancellationToken);
private string GetPackageDirectory(string packageId)
- {
- if (string.IsNullOrWhiteSpace(packageId)
- || packageId is "." or ".."
- || packageId.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0
- || packageId.Contains(Path.DirectorySeparatorChar)
- || packageId.Contains(Path.AltDirectorySeparatorChar))
- {
- throw new InvalidOperationException($"不正なNuGetパッケージIDです: {packageId}");
- }
-
- var root = this.nugetPluginsDir
- .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
- + Path.DirectorySeparatorChar;
- var packageDirectory = Path.GetFullPath(Path.Combine(root, packageId));
- if (!packageDirectory.StartsWith(root, StringComparison.OrdinalIgnoreCase))
- {
- throw new InvalidOperationException($"不正なNuGetパッケージIDです: {packageId}");
- }
-
- return packageDirectory;
- }
+ => NuGetPluginOperation.GetPackageDirectory(this.nugetPluginsDir, packageId);
private static void TryDeleteDirectory(string directory)
{
@@ -714,21 +685,6 @@ private static void TryDeleteDirectory(string directory)
}
}
- private static void TryDeleteFile(string path)
- {
- try
- {
- if (File.Exists(path))
- {
- File.Delete(path);
- }
- }
- catch
- {
- // 後始末の失敗は元の処理結果へ影響させない
- }
- }
-
private sealed record PackageMetadataResult(
NuGetPackageInfo? Package,
Exception? Error);
@@ -754,7 +710,7 @@ public record NuGetPackageInfo(
public record InstalledPackageInfo(
string Id,
string Version,
- int? HostMajorVersion = null)
+ [property: JsonRequired] int HostMajorVersion)
{
[JsonIgnore]
public bool IsCompatible { get; init; } = true;
diff --git a/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs b/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs
index f813c3a8..cb1f72dc 100644
--- a/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs
@@ -14,8 +14,7 @@ internal static bool IsVersionCompatible(VersionRange? requiredVersion, NuGetVer
=> ValidationDisabled
|| (hostVersion is not null && requiredVersion?.Satisfies(hostVersion) is not false);
- internal static bool IsHostMajorCompatible(int? installedHostMajorVersion, int hostMajorVersion)
+ internal static bool IsHostMajorCompatible(int installedHostMajorVersion, int hostMajorVersion)
=> ValidationDisabled
- || installedHostMajorVersion is null
|| installedHostMajorVersion == hostMajorVersion;
}
From 5d5ed74cc8703b63aa15834ae83fed54204ff4e1 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sun, 9 Aug 2026 14:55:59 +0900
Subject: [PATCH 18/43] =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AA=E5=AE=9F?=
=?UTF-8?q?=E8=A3=85=E3=82=92=E5=89=8A=E9=99=A4=E3=81=97=E3=81=A6=E6=A7=8B?=
=?UTF-8?q?=E6=88=90=E3=82=92=E6=95=B4=E7=90=86=E3=81=99=E3=82=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Plugins/Directory.Build.targets | 13 +-
.../NuGetPluginServiceTests.cs | 182 +++++-----
.../PluginStore/NuGetPackageInstaller.cs | 67 ++--
.../Modules/PluginStore/NuGetPluginCatalog.cs | 146 ++------
.../PluginStore/NuGetPluginOperation.cs | 320 ++++++++---------
.../Modules/PluginStore/NuGetPluginService.cs | 334 ++++--------------
.../Modules/PluginStore/PluginStoreView.xaml | 194 +++++-----
.../PluginStore/PluginStoreView.xaml.cs | 68 ----
.../PluginStore/PluginStoreViewModel.cs | 67 +---
.../PluginStore/PrioritizedPluginCatalog.cs | 26 +-
10 files changed, 476 insertions(+), 941 deletions(-)
diff --git a/Plugins/Directory.Build.targets b/Plugins/Directory.Build.targets
index ae80e3ff..7dc871b9 100644
--- a/Plugins/Directory.Build.targets
+++ b/Plugins/Directory.Build.targets
@@ -5,20 +5,13 @@
$(PackageTags);windowtranslator-plugin
README.md
- $(TargetsForTfmSpecificContentInPackage);AddPluginReadmeToPackage
$(TargetsForTfmSpecificBuildOutput);AddPluginRuntimeAssetsToPackage
-
-
-
-
- README.md
-
-
-
+
+
+
-
-
@@ -55,8 +52,8 @@
Grid.Column="0"
Margin="4"
ItemsSource="{Binding Packages}"
- SelectedItem="{Binding SelectedPackage}"
- ScrollViewer.HorizontalScrollBarVisibility="Hidden">
+ ScrollViewer.HorizontalScrollBarVisibility="Hidden"
+ SelectedItem="{Binding SelectedPackage}">
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+ Grid.Row="1"
+ Grid.Column="1"
+ HorizontalAlignment="Stretch"
+ VerticalAlignment="Top"
+ FontSize="12"
+ Text="{Binding Description}"
+ TextWrapping="Wrap" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index 8b39cf78..b7505faa 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -29,6 +29,9 @@ public partial class PluginStoreViewModel : ObservableObject, IDisposable
[NotifyPropertyChangedFor(nameof(HasError))]
private string? errorMessage;
+ [ObservableProperty]
+ private bool hideDisclaimer;
+
public bool HasError => this.ErrorMessage is not null;
public PluginPackageViewModel? SelectedPackage
@@ -392,6 +395,8 @@ public partial class PluginPackageViewModel : ObservableObject
public string Title { get; }
public string Description { get; }
public string Authors { get; }
+ public string? IconUrl { get; }
+ public bool IsOfficial { get; }
public string? ReleaseVersion { get; }
public string? PrereleaseVersion { get; }
public string? LatestVersion => this.UsePrerelease
@@ -491,6 +496,10 @@ public PluginPackageViewModel(
this.Title = info.Title;
this.Description = info.Description;
this.Authors = info.Authors;
+ this.IconUrl = info.IconUrl;
+ this.IsOfficial = info.Authors
+ .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Contains("Freesia", StringComparer.OrdinalIgnoreCase);
this.ReleaseVersion = versions
.Where(version => !version.Parsed!.IsPrerelease)
.OrderByDescending(version => version.Parsed)
diff --git a/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs b/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
index f0e21189..905a2e94 100644
--- a/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
+++ b/WindowTranslator/Modules/Settings/AllSettingsViewModel.cs
@@ -163,6 +163,7 @@ public AllSettingsViewModel(
this.target = target;
this.rootConfig = config as IConfigurationRoot;
this.PluginStore = pluginStoreViewModel;
+ this.PluginStore.HideDisclaimer = common.HidePluginStoreDisclaimer;
this.updateChecker.UpdateAvailable += UpdateChecker_UpdateAvailable;
SetUpUpdateInfo();
this.isStartup = GetIsStartup();
@@ -248,6 +249,7 @@ public async Task SaveAsync(object window)
OverlaySwitch = this.OverlaySwitch,
IsOverlayPointSwap = this.IsOverlayPointSwap,
IsEnableCaptureOverlay = this.IsEnableCaptureOverlay,
+ HidePluginStoreDisclaimer = this.PluginStore.HideDisclaimer,
},
Targets = this.Targets.ToDictionary(t => t.Name, t => new TargetSettings()
{
diff --git a/WindowTranslator/Properties/Resources.Designer.cs b/WindowTranslator/Properties/Resources.Designer.cs
index a526e1fb..ef69741b 100644
--- a/WindowTranslator/Properties/Resources.Designer.cs
+++ b/WindowTranslator/Properties/Resources.Designer.cs
@@ -467,6 +467,11 @@ internal Resources() {
///
public static string PluginStore => ResourceManager.GetString("PluginStore", resourceCulture) ?? string.Empty;
+ ///
+ /// プラグインストアの免責事項に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string PluginStoreDisclaimer => ResourceManager.GetString("PluginStoreDisclaimer", resourceCulture) ?? string.Empty;
+
///
/// "プレリリース" に類似しているローカライズされた文字列を検索します。
///
diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx
index 979d39c9..13b68e80 100644
--- a/WindowTranslator/Properties/Resources.ar.resx
+++ b/WindowTranslator/Properties/Resources.ar.resx
@@ -510,4 +510,7 @@
معلومات الترخيص
+
+ المكونات الإضافية حزم تابعة لجهات خارجية موزعة عبر NuGet. يمنح مالك كل حزمة ترخيصها، ولا يضمن WindowTranslator محتواها أو سلوكها أو أمانها. ثبّت فقط الحزم التي تثق بها.
+
diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx
index 2b2280c0..8bb0dc71 100644
--- a/WindowTranslator/Properties/Resources.cs.resx
+++ b/WindowTranslator/Properties/Resources.cs.resx
@@ -400,4 +400,7 @@ Monitory nejsou podporovány.
Informace o licenci
+
+ Pluginy jsou balíčky třetích stran distribuované prostřednictvím NuGet. Licenci ke každému balíčku uděluje jeho vlastník; WindowTranslator nezaručuje jeho obsah, chování ani bezpečnost. Instalujte pouze balíčky, kterým důvěřujete.
+
diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx
index 6381cbf3..64717862 100644
--- a/WindowTranslator/Properties/Resources.de.resx
+++ b/WindowTranslator/Properties/Resources.de.resx
@@ -519,4 +519,7 @@ Monitore werden nicht unterstützt.
Lizenzinformationen
+
+ Plugins sind Drittanbieterpakete, die über NuGet verteilt werden. Die Lizenz jedes Pakets wird vom jeweiligen Eigentümer gewährt; WindowTranslator übernimmt keine Gewähr für Inhalt, Verhalten oder Sicherheit. Installieren Sie nur vertrauenswürdige Pakete.
+
diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx
index 21dfeaaa..f6daca1b 100644
--- a/WindowTranslator/Properties/Resources.en.resx
+++ b/WindowTranslator/Properties/Resources.en.resx
@@ -519,4 +519,7 @@ Monitors are not supported.
License information
+
+ Plugins are third-party packages distributed through NuGet. Each package is licensed by its owner; WindowTranslator does not guarantee its content, behavior, or safety. Install only packages you trust.
+
diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx
index 35b9debe..1bc9d1c4 100644
--- a/WindowTranslator/Properties/Resources.es.resx
+++ b/WindowTranslator/Properties/Resources.es.resx
@@ -510,4 +510,7 @@
Información de licencia
+
+ Los complementos son paquetes de terceros distribuidos mediante NuGet. La licencia de cada paquete la concede su propietario; WindowTranslator no garantiza su contenido, funcionamiento ni seguridad. Instala solo paquetes de confianza.
+
diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx
index 006c1798..6d22385d 100644
--- a/WindowTranslator/Properties/Resources.fa.resx
+++ b/WindowTranslator/Properties/Resources.fa.resx
@@ -504,4 +504,7 @@
اطلاعات مجوز
+
+ افزونهها بستههای شخص ثالثی هستند که از طریق NuGet توزیع میشوند. مجوز هر بسته را مالک آن ارائه میکند و WindowTranslator محتوا، عملکرد یا امنیت آن را تضمین نمیکند. فقط بستههای مورد اعتماد را نصب کنید.
+
diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx
index a39ad460..cbb5cc6c 100644
--- a/WindowTranslator/Properties/Resources.fil.resx
+++ b/WindowTranslator/Properties/Resources.fil.resx
@@ -519,4 +519,7 @@ Ang monitor ay hindi suportado.
Impormasyon ng lisensya
+
+ Ang mga plugin ay mga third-party package na ipinapamahagi sa NuGet. Ang lisensya ng bawat package ay ibinibigay ng may-ari nito; hindi ginagarantiya ng WindowTranslator ang nilalaman, paggana, o kaligtasan nito. Mag-install lamang ng mga package na pinagkakatiwalaan mo.
+
diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx
index 773a85ae..8d441767 100644
--- a/WindowTranslator/Properties/Resources.fr.resx
+++ b/WindowTranslator/Properties/Resources.fr.resx
@@ -510,4 +510,7 @@
Informations de licence
+
+ Les plugins sont des paquets tiers distribués via NuGet. La licence de chaque paquet est accordée par son propriétaire ; WindowTranslator ne garantit ni son contenu, ni son fonctionnement, ni sa sécurité. N’installez que des paquets de confiance.
+
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index 04c59816..55799abc 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -512,4 +512,7 @@
लाइसेंस जानकारी
+
+ प्लगइन NuGet के माध्यम से वितरित तृतीय-पक्ष पैकेज हैं। प्रत्येक पैकेज का लाइसेंस उसके स्वामी द्वारा दिया जाता है; WindowTranslator उसकी सामग्री, व्यवहार या सुरक्षा की गारंटी नहीं देता। केवल विश्वसनीय पैकेज इंस्टॉल करें।
+
diff --git a/WindowTranslator/Properties/Resources.hu.resx b/WindowTranslator/Properties/Resources.hu.resx
index 353f559e..0142823e 100644
--- a/WindowTranslator/Properties/Resources.hu.resx
+++ b/WindowTranslator/Properties/Resources.hu.resx
@@ -400,4 +400,7 @@ A monitorok nem támogatottak.
Licencinformációk
+
+ A beépülő modulok a NuGeten keresztül terjesztett, harmadik féltől származó csomagok. Az egyes csomagok licencét a tulajdonosuk biztosítja; a WindowTranslator nem garantálja azok tartalmát, működését vagy biztonságát. Csak megbízható csomagokat telepítsen.
+
diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx
index 3543c1e7..04cca7d9 100644
--- a/WindowTranslator/Properties/Resources.id.resx
+++ b/WindowTranslator/Properties/Resources.id.resx
@@ -518,4 +518,7 @@ Monitor tidak didukung.
Informasi lisensi
+
+ Plugin adalah paket pihak ketiga yang didistribusikan melalui NuGet. Lisensi setiap paket diberikan oleh pemiliknya; WindowTranslator tidak menjamin konten, perilaku, atau keamanannya. Instal hanya paket yang Anda percayai.
+
diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx
index d1825877..1c2f29ba 100644
--- a/WindowTranslator/Properties/Resources.ko.resx
+++ b/WindowTranslator/Properties/Resources.ko.resx
@@ -519,4 +519,7 @@
라이선스 정보
+
+ 플러그인은 NuGet을 통해 배포되는 타사 패키지입니다. 각 패키지의 라이선스는 해당 소유자가 제공하며, WindowTranslator는 콘텐츠, 동작 또는 안전성을 보장하지 않습니다. 신뢰할 수 있는 패키지만 설치하세요.
+
diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx
index 7e53277f..06bc2ea4 100644
--- a/WindowTranslator/Properties/Resources.ms.resx
+++ b/WindowTranslator/Properties/Resources.ms.resx
@@ -518,4 +518,7 @@ Monitor tidak disokong.
Maklumat lesen
+
+ Pemalam ialah pakej pihak ketiga yang diedarkan melalui NuGet. Lesen setiap pakej diberikan oleh pemiliknya; WindowTranslator tidak menjamin kandungan, tingkah laku atau keselamatannya. Pasang hanya pakej yang anda percayai.
+
diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx
index f9bbba9a..9c7d3748 100644
--- a/WindowTranslator/Properties/Resources.pl.resx
+++ b/WindowTranslator/Properties/Resources.pl.resx
@@ -519,4 +519,7 @@ Monitory nie są obsługiwane.
Informacje o licencji
+
+ Wtyczki są pakietami innych firm rozpowszechnianymi przez NuGet. Licencję każdego pakietu zapewnia jego właściciel; WindowTranslator nie gwarantuje jego zawartości, działania ani bezpieczeństwa. Instaluj tylko zaufane pakiety.
+
diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx
index 4c0f7abc..a1b611df 100644
--- a/WindowTranslator/Properties/Resources.pt-BR.resx
+++ b/WindowTranslator/Properties/Resources.pt-BR.resx
@@ -518,4 +518,7 @@ Monitor tidak didukung.
Informações de licença
+
+ Os plugins são pacotes de terceiros distribuídos pelo NuGet. A licença de cada pacote é concedida pelo proprietário; o WindowTranslator não garante seu conteúdo, funcionamento ou segurança. Instale apenas pacotes confiáveis.
+
diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx
index 0774e624..95805c42 100644
--- a/WindowTranslator/Properties/Resources.resx
+++ b/WindowTranslator/Properties/Resources.resx
@@ -519,4 +519,7 @@
ライセンス情報
+
+ プラグインはNuGet上の第三者パッケージです。各パッケージのライセンスは所有者から付与され、WindowTranslatorは内容・動作・安全性を保証しません。信頼できるパッケージだけをインストールしてください。
+
diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx
index e9edbc8d..712655de 100644
--- a/WindowTranslator/Properties/Resources.ru.resx
+++ b/WindowTranslator/Properties/Resources.ru.resx
@@ -510,4 +510,7 @@
Информация о лицензии
+
+ Плагины — это сторонние пакеты, распространяемые через NuGet. Лицензию на каждый пакет предоставляет его владелец; WindowTranslator не гарантирует его содержимое, работу или безопасность. Устанавливайте только пакеты, которым доверяете.
+
diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx
index 823db23f..321a7f85 100644
--- a/WindowTranslator/Properties/Resources.th.resx
+++ b/WindowTranslator/Properties/Resources.th.resx
@@ -519,4 +519,7 @@
ข้อมูลใบอนุญาต
+
+ ปลั๊กอินเป็นแพ็กเกจของบุคคลที่สามที่เผยแพร่ผ่าน NuGet เจ้าของแพ็กเกจเป็นผู้ให้สิทธิ์การใช้งาน และ WindowTranslator ไม่รับประกันเนื้อหา การทำงาน หรือความปลอดภัย โปรดติดตั้งเฉพาะแพ็กเกจที่คุณเชื่อถือ
+
diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx
index 54e3a7e6..f8ee3b16 100644
--- a/WindowTranslator/Properties/Resources.tr.resx
+++ b/WindowTranslator/Properties/Resources.tr.resx
@@ -519,4 +519,7 @@ Monitör desteklenmiyor.
Lisans bilgileri
+
+ Eklentiler, NuGet üzerinden dağıtılan üçüncü taraf paketlerdir. Her paketin lisansı sahibi tarafından sağlanır; WindowTranslator içeriğini, davranışını veya güvenliğini garanti etmez. Yalnızca güvendiğiniz paketleri yükleyin.
+
diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx
index 3c74da9e..4a20d8f9 100644
--- a/WindowTranslator/Properties/Resources.vi.resx
+++ b/WindowTranslator/Properties/Resources.vi.resx
@@ -519,4 +519,7 @@ Màn hình không được hỗ trợ.
Thông tin giấy phép
+
+ Plugin là các gói của bên thứ ba được phân phối qua NuGet. Giấy phép của mỗi gói do chủ sở hữu cung cấp; WindowTranslator không đảm bảo nội dung, hoạt động hoặc độ an toàn của gói. Chỉ cài đặt các gói bạn tin cậy.
+
diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx
index 672a4951..9247f132 100644
--- a/WindowTranslator/Properties/Resources.zh-CN.resx
+++ b/WindowTranslator/Properties/Resources.zh-CN.resx
@@ -519,4 +519,7 @@
许可证信息
+
+ 插件是通过 NuGet 分发的第三方包。每个包的许可证由其所有者授予;WindowTranslator 不保证其内容、行为或安全性。请仅安装你信任的包。
+
diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx
index 93a76e22..57795173 100644
--- a/WindowTranslator/Properties/Resources.zh-TW.resx
+++ b/WindowTranslator/Properties/Resources.zh-TW.resx
@@ -519,4 +519,7 @@
授權資訊
+
+ 外掛程式是透過 NuGet 發佈的第三方套件。每個套件的授權由其擁有者授予;WindowTranslator 不保證其內容、行為或安全性。請只安裝您信任的套件。
+
diff --git a/WindowTranslator/WindowTranslator.csproj b/WindowTranslator/WindowTranslator.csproj
index 7cff9ac0..22d40b62 100644
--- a/WindowTranslator/WindowTranslator.csproj
+++ b/WindowTranslator/WindowTranslator.csproj
@@ -33,6 +33,7 @@
+
From 60bc741ca99da19ce1c956efd890dc7d140a79e9 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sun, 9 Aug 2026 20:16:59 +0900
Subject: [PATCH 21/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E7=8A=B6=E6=85=8B=E3=83=90=E3=83=83=E3=82=B8=E3=82=92?=
=?UTF-8?q?=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=81=AB=E9=87=8D=E3=81=AD?=
=?UTF-8?q?=E3=81=A6=E8=A1=A8=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Modules/PluginStore/PluginStoreView.xaml | 54 ++++++++++++-------
.../Properties/Resources.Designer.cs | 5 ++
WindowTranslator/Properties/Resources.ar.resx | 3 ++
WindowTranslator/Properties/Resources.cs.resx | 3 ++
WindowTranslator/Properties/Resources.de.resx | 3 ++
WindowTranslator/Properties/Resources.en.resx | 3 ++
WindowTranslator/Properties/Resources.es.resx | 3 ++
WindowTranslator/Properties/Resources.fa.resx | 3 ++
.../Properties/Resources.fil.resx | 3 ++
WindowTranslator/Properties/Resources.fr.resx | 3 ++
WindowTranslator/Properties/Resources.hi.resx | 3 ++
WindowTranslator/Properties/Resources.hu.resx | 3 ++
WindowTranslator/Properties/Resources.id.resx | 3 ++
WindowTranslator/Properties/Resources.ko.resx | 3 ++
WindowTranslator/Properties/Resources.ms.resx | 3 ++
WindowTranslator/Properties/Resources.pl.resx | 3 ++
.../Properties/Resources.pt-BR.resx | 3 ++
WindowTranslator/Properties/Resources.resx | 3 ++
WindowTranslator/Properties/Resources.ru.resx | 3 ++
WindowTranslator/Properties/Resources.th.resx | 3 ++
WindowTranslator/Properties/Resources.tr.resx | 3 ++
WindowTranslator/Properties/Resources.vi.resx | 3 ++
.../Properties/Resources.zh-CN.resx | 3 ++
.../Properties/Resources.zh-TW.resx | 3 ++
24 files changed, 107 insertions(+), 18 deletions(-)
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
index 9dc5d7e1..3e9f4025 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
@@ -95,27 +95,45 @@
RenderOptions.BitmapScalingMode="HighQuality"
Source="{Binding IconUrl}"
Stretch="UniformToFill" />
+
+
+
+
+
+
-
-
@@ -134,7 +152,7 @@
Filled="True"
Foreground="{ui:ThemeResource AccentTextFillColorSecondaryBrush}"
Symbol="CheckmarkCircle24"
- ToolTip="{Binding Authors}"
+ ToolTip="{x:Static properties:Resources.PluginOfficial}"
Visibility="{Binding IsOfficial, Converter={StaticResource b2vConv}}" />
@@ -246,7 +264,7 @@
Filled="True"
Foreground="{ui:ThemeResource AccentTextFillColorSecondaryBrush}"
Symbol="CheckmarkCircle24"
- ToolTip="{Binding Authors}"
+ ToolTip="{x:Static properties:Resources.PluginOfficial}"
Visibility="{Binding IsOfficial, Converter={StaticResource b2vConv}}" />
public static string PluginStoreDisclaimer => ResourceManager.GetString("PluginStoreDisclaimer", resourceCulture) ?? string.Empty;
+ ///
+ /// "公式プラグイン" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string PluginOfficial => ResourceManager.GetString("PluginOfficial", resourceCulture) ?? string.Empty;
+
///
/// "プレリリース" に類似しているローカライズされた文字列を検索します。
///
diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx
index 13b68e80..253acc01 100644
--- a/WindowTranslator/Properties/Resources.ar.resx
+++ b/WindowTranslator/Properties/Resources.ar.resx
@@ -513,4 +513,7 @@
المكونات الإضافية حزم تابعة لجهات خارجية موزعة عبر NuGet. يمنح مالك كل حزمة ترخيصها، ولا يضمن WindowTranslator محتواها أو سلوكها أو أمانها. ثبّت فقط الحزم التي تثق بها.
+
+ مكوّن إضافي رسمي
+
diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx
index 8bb0dc71..3772ad36 100644
--- a/WindowTranslator/Properties/Resources.cs.resx
+++ b/WindowTranslator/Properties/Resources.cs.resx
@@ -403,4 +403,7 @@ Monitory nejsou podporovány.
Pluginy jsou balíčky třetích stran distribuované prostřednictvím NuGet. Licenci ke každému balíčku uděluje jeho vlastník; WindowTranslator nezaručuje jeho obsah, chování ani bezpečnost. Instalujte pouze balíčky, kterým důvěřujete.
+
+ Oficiální plugin
+
diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx
index 64717862..f20114c6 100644
--- a/WindowTranslator/Properties/Resources.de.resx
+++ b/WindowTranslator/Properties/Resources.de.resx
@@ -522,4 +522,7 @@ Monitore werden nicht unterstützt.
Plugins sind Drittanbieterpakete, die über NuGet verteilt werden. Die Lizenz jedes Pakets wird vom jeweiligen Eigentümer gewährt; WindowTranslator übernimmt keine Gewähr für Inhalt, Verhalten oder Sicherheit. Installieren Sie nur vertrauenswürdige Pakete.
+
+ Offizielles Plugin
+
diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx
index f6daca1b..39320722 100644
--- a/WindowTranslator/Properties/Resources.en.resx
+++ b/WindowTranslator/Properties/Resources.en.resx
@@ -522,4 +522,7 @@ Monitors are not supported.
Plugins are third-party packages distributed through NuGet. Each package is licensed by its owner; WindowTranslator does not guarantee its content, behavior, or safety. Install only packages you trust.
+
+ Official plugin
+
diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx
index 1bc9d1c4..7911842d 100644
--- a/WindowTranslator/Properties/Resources.es.resx
+++ b/WindowTranslator/Properties/Resources.es.resx
@@ -513,4 +513,7 @@
Los complementos son paquetes de terceros distribuidos mediante NuGet. La licencia de cada paquete la concede su propietario; WindowTranslator no garantiza su contenido, funcionamiento ni seguridad. Instala solo paquetes de confianza.
+
+ Complemento oficial
+
diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx
index 6d22385d..8201f8a5 100644
--- a/WindowTranslator/Properties/Resources.fa.resx
+++ b/WindowTranslator/Properties/Resources.fa.resx
@@ -507,4 +507,7 @@
افزونهها بستههای شخص ثالثی هستند که از طریق NuGet توزیع میشوند. مجوز هر بسته را مالک آن ارائه میکند و WindowTranslator محتوا، عملکرد یا امنیت آن را تضمین نمیکند. فقط بستههای مورد اعتماد را نصب کنید.
+
+ افزونه رسمی
+
diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx
index cbb5cc6c..70086f03 100644
--- a/WindowTranslator/Properties/Resources.fil.resx
+++ b/WindowTranslator/Properties/Resources.fil.resx
@@ -522,4 +522,7 @@ Ang monitor ay hindi suportado.
Ang mga plugin ay mga third-party package na ipinapamahagi sa NuGet. Ang lisensya ng bawat package ay ibinibigay ng may-ari nito; hindi ginagarantiya ng WindowTranslator ang nilalaman, paggana, o kaligtasan nito. Mag-install lamang ng mga package na pinagkakatiwalaan mo.
+
+ Opisyal na plugin
+
diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx
index 8d441767..4e3b9f8b 100644
--- a/WindowTranslator/Properties/Resources.fr.resx
+++ b/WindowTranslator/Properties/Resources.fr.resx
@@ -513,4 +513,7 @@
Les plugins sont des paquets tiers distribués via NuGet. La licence de chaque paquet est accordée par son propriétaire ; WindowTranslator ne garantit ni son contenu, ni son fonctionnement, ni sa sécurité. N’installez que des paquets de confiance.
+
+ Plugin officiel
+
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index 55799abc..d4d8c0ea 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -515,4 +515,7 @@
प्लगइन NuGet के माध्यम से वितरित तृतीय-पक्ष पैकेज हैं। प्रत्येक पैकेज का लाइसेंस उसके स्वामी द्वारा दिया जाता है; WindowTranslator उसकी सामग्री, व्यवहार या सुरक्षा की गारंटी नहीं देता। केवल विश्वसनीय पैकेज इंस्टॉल करें।
+
+ आधिकारिक प्लगइन
+
diff --git a/WindowTranslator/Properties/Resources.hu.resx b/WindowTranslator/Properties/Resources.hu.resx
index 0142823e..8f7416cf 100644
--- a/WindowTranslator/Properties/Resources.hu.resx
+++ b/WindowTranslator/Properties/Resources.hu.resx
@@ -403,4 +403,7 @@ A monitorok nem támogatottak.
A beépülő modulok a NuGeten keresztül terjesztett, harmadik féltől származó csomagok. Az egyes csomagok licencét a tulajdonosuk biztosítja; a WindowTranslator nem garantálja azok tartalmát, működését vagy biztonságát. Csak megbízható csomagokat telepítsen.
+
+ Hivatalos beépülő modul
+
diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx
index 04cca7d9..a0cbeee6 100644
--- a/WindowTranslator/Properties/Resources.id.resx
+++ b/WindowTranslator/Properties/Resources.id.resx
@@ -521,4 +521,7 @@ Monitor tidak didukung.
Plugin adalah paket pihak ketiga yang didistribusikan melalui NuGet. Lisensi setiap paket diberikan oleh pemiliknya; WindowTranslator tidak menjamin konten, perilaku, atau keamanannya. Instal hanya paket yang Anda percayai.
+
+ Plugin resmi
+
diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx
index 1c2f29ba..f9a75f5c 100644
--- a/WindowTranslator/Properties/Resources.ko.resx
+++ b/WindowTranslator/Properties/Resources.ko.resx
@@ -522,4 +522,7 @@
플러그인은 NuGet을 통해 배포되는 타사 패키지입니다. 각 패키지의 라이선스는 해당 소유자가 제공하며, WindowTranslator는 콘텐츠, 동작 또는 안전성을 보장하지 않습니다. 신뢰할 수 있는 패키지만 설치하세요.
+
+ 공식 플러그인
+
diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx
index 06bc2ea4..dafec169 100644
--- a/WindowTranslator/Properties/Resources.ms.resx
+++ b/WindowTranslator/Properties/Resources.ms.resx
@@ -521,4 +521,7 @@ Monitor tidak disokong.
Pemalam ialah pakej pihak ketiga yang diedarkan melalui NuGet. Lesen setiap pakej diberikan oleh pemiliknya; WindowTranslator tidak menjamin kandungan, tingkah laku atau keselamatannya. Pasang hanya pakej yang anda percayai.
+
+ Pemalam rasmi
+
diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx
index 9c7d3748..5ccda834 100644
--- a/WindowTranslator/Properties/Resources.pl.resx
+++ b/WindowTranslator/Properties/Resources.pl.resx
@@ -522,4 +522,7 @@ Monitory nie są obsługiwane.
Wtyczki są pakietami innych firm rozpowszechnianymi przez NuGet. Licencję każdego pakietu zapewnia jego właściciel; WindowTranslator nie gwarantuje jego zawartości, działania ani bezpieczeństwa. Instaluj tylko zaufane pakiety.
+
+ Oficjalna wtyczka
+
diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx
index a1b611df..ddb0d14d 100644
--- a/WindowTranslator/Properties/Resources.pt-BR.resx
+++ b/WindowTranslator/Properties/Resources.pt-BR.resx
@@ -521,4 +521,7 @@ Monitor tidak didukung.
Os plugins são pacotes de terceiros distribuídos pelo NuGet. A licença de cada pacote é concedida pelo proprietário; o WindowTranslator não garante seu conteúdo, funcionamento ou segurança. Instale apenas pacotes confiáveis.
+
+ Plugin oficial
+
diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx
index 95805c42..f15c267a 100644
--- a/WindowTranslator/Properties/Resources.resx
+++ b/WindowTranslator/Properties/Resources.resx
@@ -522,4 +522,7 @@
プラグインはNuGet上の第三者パッケージです。各パッケージのライセンスは所有者から付与され、WindowTranslatorは内容・動作・安全性を保証しません。信頼できるパッケージだけをインストールしてください。
+
+ 公式プラグイン
+
diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx
index 712655de..6bec581b 100644
--- a/WindowTranslator/Properties/Resources.ru.resx
+++ b/WindowTranslator/Properties/Resources.ru.resx
@@ -513,4 +513,7 @@
Плагины — это сторонние пакеты, распространяемые через NuGet. Лицензию на каждый пакет предоставляет его владелец; WindowTranslator не гарантирует его содержимое, работу или безопасность. Устанавливайте только пакеты, которым доверяете.
+
+ Официальный плагин
+
diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx
index 321a7f85..b1a3c09d 100644
--- a/WindowTranslator/Properties/Resources.th.resx
+++ b/WindowTranslator/Properties/Resources.th.resx
@@ -522,4 +522,7 @@
ปลั๊กอินเป็นแพ็กเกจของบุคคลที่สามที่เผยแพร่ผ่าน NuGet เจ้าของแพ็กเกจเป็นผู้ให้สิทธิ์การใช้งาน และ WindowTranslator ไม่รับประกันเนื้อหา การทำงาน หรือความปลอดภัย โปรดติดตั้งเฉพาะแพ็กเกจที่คุณเชื่อถือ
+
+ ปลั๊กอินอย่างเป็นทางการ
+
diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx
index f8ee3b16..9451921f 100644
--- a/WindowTranslator/Properties/Resources.tr.resx
+++ b/WindowTranslator/Properties/Resources.tr.resx
@@ -522,4 +522,7 @@ Monitör desteklenmiyor.
Eklentiler, NuGet üzerinden dağıtılan üçüncü taraf paketlerdir. Her paketin lisansı sahibi tarafından sağlanır; WindowTranslator içeriğini, davranışını veya güvenliğini garanti etmez. Yalnızca güvendiğiniz paketleri yükleyin.
+
+ Resmî eklenti
+
diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx
index 4a20d8f9..0b6c42aa 100644
--- a/WindowTranslator/Properties/Resources.vi.resx
+++ b/WindowTranslator/Properties/Resources.vi.resx
@@ -522,4 +522,7 @@ Màn hình không được hỗ trợ.
Plugin là các gói của bên thứ ba được phân phối qua NuGet. Giấy phép của mỗi gói do chủ sở hữu cung cấp; WindowTranslator không đảm bảo nội dung, hoạt động hoặc độ an toàn của gói. Chỉ cài đặt các gói bạn tin cậy.
+
+ Plugin chính thức
+
diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx
index 9247f132..1a3e255a 100644
--- a/WindowTranslator/Properties/Resources.zh-CN.resx
+++ b/WindowTranslator/Properties/Resources.zh-CN.resx
@@ -522,4 +522,7 @@
插件是通过 NuGet 分发的第三方包。每个包的许可证由其所有者授予;WindowTranslator 不保证其内容、行为或安全性。请仅安装你信任的包。
+
+ 官方插件
+
diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx
index 57795173..b02a632b 100644
--- a/WindowTranslator/Properties/Resources.zh-TW.resx
+++ b/WindowTranslator/Properties/Resources.zh-TW.resx
@@ -522,4 +522,7 @@
外掛程式是透過 NuGet 發佈的第三方套件。每個套件的授權由其擁有者授予;WindowTranslator 不保證其內容、行為或安全性。請只安裝您信任的套件。
+
+ 官方外掛程式
+
From b313bf29e1ec5537ceb98db5dd07669f23691789 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sun, 9 Aug 2026 23:18:53 +0900
Subject: [PATCH 22/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=82=B9=E3=83=88=E3=82=A2=E3=81=AE=E3=83=AC=E3=83=93?=
=?UTF-8?q?=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../NuGetPluginServiceTests.cs | 45 ++++++++++++++++++-
.../PluginStore/NuGetPluginOperation.cs | 5 ++-
.../Modules/PluginStore/NuGetPluginService.cs | 15 ++++++-
.../PluginStore/PluginStoreViewModel.cs | 20 ---------
.../Properties/Resources.Designer.cs | 15 -------
WindowTranslator/Properties/Resources.ar.resx | 9 ----
WindowTranslator/Properties/Resources.cs.resx | 9 ----
WindowTranslator/Properties/Resources.de.resx | 9 ----
WindowTranslator/Properties/Resources.en.resx | 9 ----
WindowTranslator/Properties/Resources.es.resx | 9 ----
WindowTranslator/Properties/Resources.fa.resx | 9 ----
.../Properties/Resources.fil.resx | 9 ----
WindowTranslator/Properties/Resources.fr.resx | 9 ----
WindowTranslator/Properties/Resources.hi.resx | 9 ----
WindowTranslator/Properties/Resources.hu.resx | 9 ----
WindowTranslator/Properties/Resources.id.resx | 9 ----
WindowTranslator/Properties/Resources.ko.resx | 9 ----
WindowTranslator/Properties/Resources.ms.resx | 9 ----
WindowTranslator/Properties/Resources.pl.resx | 9 ----
.../Properties/Resources.pt-BR.resx | 9 ----
WindowTranslator/Properties/Resources.resx | 9 ----
WindowTranslator/Properties/Resources.ru.resx | 9 ----
WindowTranslator/Properties/Resources.th.resx | 9 ----
WindowTranslator/Properties/Resources.tr.resx | 9 ----
WindowTranslator/Properties/Resources.vi.resx | 9 ----
.../Properties/Resources.zh-CN.resx | 9 ----
.../Properties/Resources.zh-TW.resx | 9 ----
docs/plugin.md | 3 +-
28 files changed, 63 insertions(+), 238 deletions(-)
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index 0b9c93a1..64336489 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -832,7 +832,6 @@ public void IncompatibleInstalledPackageCanReinstallACompatibleVersion()
Assert.False(package.IsUpdateAvailable);
Assert.True(package.RequiresReinstall);
Assert.True(package.CanUpdate);
- Assert.Equal(WindowTranslator.Properties.Resources.PluginIncompatible, package.StatusText);
package.IsCompatible = true;
@@ -1354,6 +1353,50 @@ await NuGetPluginOperation.SaveManifestAsync(
}
}
+ [Fact]
+ public async Task CompletedInstallRemainsLoadableWhenOperationCleanupFails()
+ {
+ var sourceDirectory = CreateTestDirectory();
+ try
+ {
+ const string packageId = "Root.Plugin";
+ var manifest = new InstalledManifest(
+ [new InstalledPackageInfo(packageId, "2.0.0", HostMajorVersion: 1)]);
+ var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
+ var operation = await NuGetPluginOperation.BeginAsync(
+ sourceDirectory,
+ packageId,
+ originalManifest: null,
+ CancellationToken.None);
+ Directory.Move(operation.WorkingPath, operation.TargetPath);
+ File.WriteAllText(Path.Combine(operation.TargetPath, "plugin.txt"), "new");
+ await NuGetPluginOperation.SaveManifestAsync(
+ manifestPath,
+ manifest,
+ CancellationToken.None);
+ operation.Commit();
+
+ Directory.CreateDirectory(operation.BackupPath);
+ var lockedPath = Path.Combine(operation.BackupPath, "locked.txt");
+ await File.WriteAllTextAsync(lockedPath, "locked");
+ await using (File.Open(lockedPath, FileMode.Open, FileAccess.Read, FileShare.None))
+ {
+ var unresolved = await NuGetPluginOperation.RecoverInterruptedOperationsAsync(
+ sourceDirectory);
+
+ Assert.Empty(unresolved);
+ Assert.Equal("new", File.ReadAllText(Path.Combine(operation.TargetPath, "plugin.txt")));
+ }
+
+ Assert.Empty(await NuGetPluginOperation.RecoverInterruptedOperationsAsync(sourceDirectory));
+ Assert.False(Directory.Exists(operation.BackupPath));
+ }
+ finally
+ {
+ DeleteTestDirectory(sourceDirectory);
+ }
+ }
+
[Fact]
public void CatalogSynchronizationFollowsCompatibilityValidationSetting()
{
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs
index d75abcac..b8fdaaf4 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs
@@ -133,10 +133,11 @@ internal static async Task> RecoverInterruptedOperationsAsy
{
cancellationToken.ThrowIfCancellationRequested();
NuGetPluginOperationState? state = null;
+ var isCommitted = false;
try
{
var committedPath = Path.Combine(operationDirectory, CommittedFileName);
- var isCommitted = File.Exists(committedPath);
+ isCommitted = File.Exists(committedPath);
var statePath = isCommitted
? committedPath
: Path.Combine(operationDirectory, PendingFileName);
@@ -166,7 +167,7 @@ await File.ReadAllTextAsync(statePath, cancellationToken).ConfigureAwait(false),
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
- if (!string.IsNullOrWhiteSpace(state?.PackageId))
+ if (!isCommitted && !string.IsNullOrWhiteSpace(state?.PackageId))
{
unresolvedPackageIds.Add(state.PackageId);
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index e9e81ea7..22c41019 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -46,6 +46,7 @@ public sealed class NuGetPluginService : BackgroundService
private readonly AsyncSemaphore refreshLock = new(1);
private readonly object snapshotLock = new();
private PluginStoreSnapshot packageSnapshot = PluginStoreSnapshot.Empty;
+ private int disposed;
internal NuGetPluginService(
ILogger logger,
@@ -144,7 +145,7 @@ internal async Task RefreshPackageInformationAsync(CancellationToken cancellatio
.ToArray();
this.logger.LogInformation("NuGetタグ検索完了: {Count}件の候補が見つかりました。", searchResults.Length);
- var requestGate = new AsyncSemaphore(MaxConcurrentMetadataRequests);
+ using var requestGate = new AsyncSemaphore(MaxConcurrentMetadataRequests);
var packageTasks = searchResults.Select(async data =>
{
using var request = await requestGate.EnterAsync(cancellationToken);
@@ -481,6 +482,18 @@ private Task SaveManifestAsync(InstalledManifest manifest, CancellationToken can
manifest,
cancellationToken);
+ public override void Dispose()
+ {
+ if (Interlocked.Exchange(ref this.disposed, 1) != 0)
+ {
+ return;
+ }
+
+ base.Dispose();
+ this.operationLock.Dispose();
+ this.refreshLock.Dispose();
+ }
+
}
/// NuGetパッケージ情報
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index b7505faa..2a53d7a0 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -424,16 +424,13 @@ public partial class PluginPackageViewModel : ObservableObject
public bool IsNotInstalled => !this.IsInstalled;
[ObservableProperty]
- [NotifyPropertyChangedFor(nameof(StatusText))]
private string? installedVersion;
[ObservableProperty]
- [NotifyPropertyChangedFor(nameof(StatusText))]
[NotifyPropertyChangedFor(nameof(CanUpdate))]
private bool isUpdateAvailable;
[ObservableProperty]
- [NotifyPropertyChangedFor(nameof(StatusText))]
[NotifyPropertyChangedFor(nameof(RequiresReinstall))]
[NotifyPropertyChangedFor(nameof(CanUpdate))]
private bool isCompatible;
@@ -455,29 +452,12 @@ public partial class PluginPackageViewModel : ObservableObject
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(LatestVersion))]
[NotifyPropertyChangedFor(nameof(CanInstall))]
- [NotifyPropertyChangedFor(nameof(StatusText))]
[NotifyPropertyChangedFor(nameof(RequiresReinstall))]
[NotifyPropertyChangedFor(nameof(CanUpdate))]
private bool usePrerelease;
public bool HasReadme => !string.IsNullOrWhiteSpace(this.ReadmeMarkdown);
- public string StatusText
- {
- get
- {
- if (this.IsInstalled && !this.IsCompatible)
- return Resources.PluginIncompatible;
- if (this.IsUpdateAvailable
- && this.InstalledVersion is not null
- && this.LatestVersion is not null)
- return string.Format(Properties.Resources.UpdateAvailableVersion, this.InstalledVersion, this.LatestVersion);
- if (this.IsInstalled && this.InstalledVersion is not null)
- return string.Format(Properties.Resources.InstalledVersion, this.InstalledVersion);
- return string.Empty;
- }
- }
-
public PluginPackageViewModel(
NuGetPackageInfo info,
bool isInstalled,
diff --git a/WindowTranslator/Properties/Resources.Designer.cs b/WindowTranslator/Properties/Resources.Designer.cs
index c2719e81..3c016b28 100644
--- a/WindowTranslator/Properties/Resources.Designer.cs
+++ b/WindowTranslator/Properties/Resources.Designer.cs
@@ -292,11 +292,6 @@ internal Resources() {
///
public static string Installed => ResourceManager.GetString("Installed", resourceCulture) ?? string.Empty;
- ///
- /// "インストール済み: {0}" に類似しているローカライズされた文字列を検索します。
- ///
- public static string InstalledVersion => ResourceManager.GetString("InstalledVersion", resourceCulture) ?? string.Empty;
-
///
/// "インストール済みバージョン" に類似しているローカライズされた文字列を検索します。
///
@@ -457,11 +452,6 @@ internal Resources() {
///
public static string PluginInstallSuccess => ResourceManager.GetString("PluginInstallSuccess", resourceCulture) ?? string.Empty;
- ///
- /// "現在のWindowTranslatorメジャーバージョンとは互換性がありません。" に類似しているローカライズされた文字列を検索します。
- ///
- public static string PluginIncompatible => ResourceManager.GetString("PluginIncompatible", resourceCulture) ?? string.Empty;
-
///
/// "プラグイン" に類似しているローカライズされた文字列を検索します。
///
@@ -672,11 +662,6 @@ internal Resources() {
///
public static string UpdateAvailable => ResourceManager.GetString("UpdateAvailable", resourceCulture) ?? string.Empty;
- ///
- /// "インストール済み: {0} → 最新: {1}" に類似しているローカライズされた文字列を検索します。
- ///
- public static string UpdateAvailableVersion => ResourceManager.GetString("UpdateAvailableVersion", resourceCulture) ?? string.Empty;
-
///
/// "最新バージョンに更新" に類似しているローカライズされた文字列を検索します。
///
diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx
index 253acc01..e174d2d5 100644
--- a/WindowTranslator/Properties/Resources.ar.resx
+++ b/WindowTranslator/Properties/Resources.ar.resx
@@ -474,12 +474,6 @@
يتوفر تحديث
-
- مثبت: {0} → الأحدث: {1}
-
-
- مثبت: {0}
-
الإصدار المثبت
@@ -495,9 +489,6 @@
فشل التثبيت
-
- هذا المكون الإضافي غير متوافق مع الإصدار الرئيسي الحالي من WindowTranslator.
-
إعادة التشغيل الآن
diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx
index 3772ad36..92d6c6d8 100644
--- a/WindowTranslator/Properties/Resources.cs.resx
+++ b/WindowTranslator/Properties/Resources.cs.resx
@@ -364,12 +364,6 @@ Monitory nejsou podporovány.
Dostupná aktualizace
-
- Nainstalováno: {0} → Nejnovější: {1}
-
-
- Nainstalováno: {0}
-
Nainstalovaná verze
@@ -385,9 +379,6 @@ Monitory nejsou podporovány.
Instalace se nezdařila
-
- Tento plugin není kompatibilní s aktuální hlavní verzí aplikace WindowTranslator.
-
Restartovat nyní
diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx
index f20114c6..be73f32c 100644
--- a/WindowTranslator/Properties/Resources.de.resx
+++ b/WindowTranslator/Properties/Resources.de.resx
@@ -483,12 +483,6 @@ Monitore werden nicht unterstützt.
Update verfügbar
-
- Installiert: {0} → Aktuell: {1}
-
-
- Installiert: {0}
-
Installierte Version
@@ -504,9 +498,6 @@ Monitore werden nicht unterstützt.
Installation fehlgeschlagen
-
- Dieses Plugin ist mit der aktuellen Hauptversion von WindowTranslator nicht kompatibel.
-
Jetzt neu starten
diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx
index 39320722..09b72eee 100644
--- a/WindowTranslator/Properties/Resources.en.resx
+++ b/WindowTranslator/Properties/Resources.en.resx
@@ -483,12 +483,6 @@ Monitors are not supported.
Update available
-
- Installed: {0} → Latest: {1}
-
-
- Installed: {0}
-
Installed version
@@ -504,9 +498,6 @@ Monitors are not supported.
Installation failed
-
- This plugin is incompatible with the current WindowTranslator major version.
-
Restart now
diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx
index 7911842d..034bef2e 100644
--- a/WindowTranslator/Properties/Resources.es.resx
+++ b/WindowTranslator/Properties/Resources.es.resx
@@ -474,12 +474,6 @@
Actualización disponible
-
- Instalado: {0} → Último: {1}
-
-
- Instalado: {0}
-
Versión instalada
@@ -495,9 +489,6 @@
Error de instalación
-
- Este plugin no es compatible con la versión principal actual de WindowTranslator.
-
Reiniciar ahora
diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx
index 8201f8a5..1e8c3988 100644
--- a/WindowTranslator/Properties/Resources.fa.resx
+++ b/WindowTranslator/Properties/Resources.fa.resx
@@ -468,12 +468,6 @@
بهروزرسانی موجود است
-
- نصب شده: {0} → جدیدترین: {1}
-
-
- نصب شده: {0}
-
نسخه نصب شده
@@ -489,9 +483,6 @@
نصب ناموفق بود
-
- این افزونه با نسخه اصلی فعلی WindowTranslator سازگار نیست.
-
اکنون راهاندازی مجدد شود
diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx
index 70086f03..25f82960 100644
--- a/WindowTranslator/Properties/Resources.fil.resx
+++ b/WindowTranslator/Properties/Resources.fil.resx
@@ -483,12 +483,6 @@ Ang monitor ay hindi suportado.
Available ang update
-
- Naka-install: {0} → Pinakabago: {1}
-
-
- Naka-install: {0}
-
Naka-install na bersyon
@@ -504,9 +498,6 @@ Ang monitor ay hindi suportado.
Nabigo ang pag-install
-
- Hindi tugma ang plugin na ito sa kasalukuyang pangunahing bersyon ng WindowTranslator.
-
I-restart ngayon
diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx
index 4e3b9f8b..eca6b01d 100644
--- a/WindowTranslator/Properties/Resources.fr.resx
+++ b/WindowTranslator/Properties/Resources.fr.resx
@@ -474,12 +474,6 @@
Mise à jour disponible
-
- Installé : {0} → Dernier : {1}
-
-
- Installé : {0}
-
Version installée
@@ -495,9 +489,6 @@
Échec de l'installation
-
- Ce plugin n’est pas compatible avec la version majeure actuelle de WindowTranslator.
-
Redémarrer maintenant
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index d4d8c0ea..a42b8a23 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -476,12 +476,6 @@
अपडेट उपलब्ध
-
- इंस्टॉल: {0} → नवीनतम: {1}
-
-
- इंस्टॉल: {0}
-
इंस्टॉल किया गया संस्करण
@@ -497,9 +491,6 @@
इंस्टॉलेशन विफल
-
- यह प्लगइन WindowTranslator के वर्तमान प्रमुख संस्करण के साथ संगत नहीं है।
-
अभी पुनः आरंभ करें
diff --git a/WindowTranslator/Properties/Resources.hu.resx b/WindowTranslator/Properties/Resources.hu.resx
index 8f7416cf..65c0a1db 100644
--- a/WindowTranslator/Properties/Resources.hu.resx
+++ b/WindowTranslator/Properties/Resources.hu.resx
@@ -364,12 +364,6 @@ A monitorok nem támogatottak.
Frissítés érhető el
-
- Telepítve: {0} → Legújabb: {1}
-
-
- Telepítve: {0}
-
Telepített verzió
@@ -385,9 +379,6 @@ A monitorok nem támogatottak.
A telepítés sikertelen
-
- Ez a bővítmény nem kompatibilis a WindowTranslator jelenlegi főverziójával.
-
Újraindítás most
diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx
index a0cbeee6..100d6555 100644
--- a/WindowTranslator/Properties/Resources.id.resx
+++ b/WindowTranslator/Properties/Resources.id.resx
@@ -482,12 +482,6 @@ Monitor tidak didukung.
Pembaruan tersedia
-
- Terpasang: {0} → Terbaru: {1}
-
-
- Terpasang: {0}
-
Versi terpasang
@@ -503,9 +497,6 @@ Monitor tidak didukung.
Instalasi gagal
-
- Plugin ini tidak kompatibel dengan versi mayor WindowTranslator saat ini.
-
Mulai ulang sekarang
diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx
index f9a75f5c..3164353b 100644
--- a/WindowTranslator/Properties/Resources.ko.resx
+++ b/WindowTranslator/Properties/Resources.ko.resx
@@ -483,12 +483,6 @@
업데이트 있음
-
- 설치됨: {0} → 최신: {1}
-
-
- 설치됨: {0}
-
설치된 버전
@@ -504,9 +498,6 @@
설치 실패
-
- 이 플러그인은 현재 WindowTranslator 주 버전과 호환되지 않습니다.
-
지금 다시 시작
diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx
index dafec169..363d5c56 100644
--- a/WindowTranslator/Properties/Resources.ms.resx
+++ b/WindowTranslator/Properties/Resources.ms.resx
@@ -482,12 +482,6 @@ Monitor tidak disokong.
Kemaskini tersedia
-
- Dipasang: {0} → Terkini: {1}
-
-
- Dipasang: {0}
-
Versi yang dipasang
@@ -503,9 +497,6 @@ Monitor tidak disokong.
Pemasangan gagal
-
- Pemalam ini tidak serasi dengan versi utama WindowTranslator semasa.
-
Mulakan semula sekarang
diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx
index 5ccda834..728761f7 100644
--- a/WindowTranslator/Properties/Resources.pl.resx
+++ b/WindowTranslator/Properties/Resources.pl.resx
@@ -483,12 +483,6 @@ Monitory nie są obsługiwane.
Dostępna aktualizacja
-
- Zainstalowano: {0} → Najnowszy: {1}
-
-
- Zainstalowano: {0}
-
Zainstalowana wersja
@@ -504,9 +498,6 @@ Monitory nie są obsługiwane.
Instalacja nie powiodła się
-
- Ta wtyczka nie jest zgodna z bieżącą główną wersją WindowTranslator.
-
Uruchom ponownie teraz
diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx
index ddb0d14d..3926b627 100644
--- a/WindowTranslator/Properties/Resources.pt-BR.resx
+++ b/WindowTranslator/Properties/Resources.pt-BR.resx
@@ -482,12 +482,6 @@ Monitor tidak didukung.
Atualização disponível
-
- Instalado: {0} → Mais recente: {1}
-
-
- Instalado: {0}
-
Versão instalada
@@ -503,9 +497,6 @@ Monitor tidak didukung.
Falha na instalação
-
- Este plugin não é compatível com a versão principal atual do WindowTranslator.
-
Reiniciar agora
diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx
index f15c267a..37438d9c 100644
--- a/WindowTranslator/Properties/Resources.resx
+++ b/WindowTranslator/Properties/Resources.resx
@@ -483,12 +483,6 @@
更新あり
-
- インストール済み: {0} → 最新: {1}
-
-
- インストール済み: {0}
-
インストール済みバージョン
@@ -504,9 +498,6 @@
インストール失敗
-
- 現在のWindowTranslatorメジャーバージョンとは互換性がありません。
-
今すぐ再起動
diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx
index 6bec581b..e7db10dd 100644
--- a/WindowTranslator/Properties/Resources.ru.resx
+++ b/WindowTranslator/Properties/Resources.ru.resx
@@ -474,12 +474,6 @@
Доступно обновление
-
- Установлен: {0} → Последний: {1}
-
-
- Установлен: {0}
-
Установленная версия
@@ -495,9 +489,6 @@
Ошибка установки
-
- Этот плагин несовместим с текущей основной версией WindowTranslator.
-
Перезапустить сейчас
diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx
index b1a3c09d..67a1d993 100644
--- a/WindowTranslator/Properties/Resources.th.resx
+++ b/WindowTranslator/Properties/Resources.th.resx
@@ -483,12 +483,6 @@
มีการอัปเดต
-
- ติดตั้งแล้ว: {0} → ล่าสุด: {1}
-
-
- ติดตั้งแล้ว: {0}
-
เวอร์ชันที่ติดตั้ง
@@ -504,9 +498,6 @@
การติดตั้งล้มเหลว
-
- ปลั๊กอินนี้ไม่เข้ากันกับ WindowTranslator เวอร์ชันหลักปัจจุบัน
-
เริ่มใหม่ตอนนี้
diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx
index 9451921f..78a67cff 100644
--- a/WindowTranslator/Properties/Resources.tr.resx
+++ b/WindowTranslator/Properties/Resources.tr.resx
@@ -483,12 +483,6 @@ Monitör desteklenmiyor.
Güncelleme mevcut
-
- Yüklü: {0} → En son: {1}
-
-
- Yüklü: {0}
-
Yüklü sürüm
@@ -504,9 +498,6 @@ Monitör desteklenmiyor.
Kurulum başarısız
-
- Bu eklenti, WindowTranslator'ın mevcut ana sürümüyle uyumlu değil.
-
Şimdi yeniden başlat
diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx
index 0b6c42aa..33b5a1b0 100644
--- a/WindowTranslator/Properties/Resources.vi.resx
+++ b/WindowTranslator/Properties/Resources.vi.resx
@@ -483,12 +483,6 @@ Màn hình không được hỗ trợ.
Có cập nhật
-
- Đã cài: {0} → Mới nhất: {1}
-
-
- Đã cài: {0}
-
Phiên bản đã cài
@@ -504,9 +498,6 @@ Màn hình không được hỗ trợ.
Cài đặt thất bại
-
- Plugin này không tương thích với phiên bản chính hiện tại của WindowTranslator.
-
Khởi động lại ngay
diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx
index 1a3e255a..759315e5 100644
--- a/WindowTranslator/Properties/Resources.zh-CN.resx
+++ b/WindowTranslator/Properties/Resources.zh-CN.resx
@@ -483,12 +483,6 @@
有更新
-
- 已安装: {0} → 最新: {1}
-
-
- 已安装: {0}
-
已安装版本
@@ -504,9 +498,6 @@
安装失败
-
- 此插件与当前 WindowTranslator 主版本不兼容。
-
立即重启
diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx
index b02a632b..5317f659 100644
--- a/WindowTranslator/Properties/Resources.zh-TW.resx
+++ b/WindowTranslator/Properties/Resources.zh-TW.resx
@@ -483,12 +483,6 @@
有更新
-
- 已安裝: {0} → 最新: {1}
-
-
- 已安裝: {0}
-
已安裝版本
@@ -504,9 +498,6 @@
安裝失敗
-
- 此外掛程式與目前的 WindowTranslator 主要版本不相容。
-
立即重新啟動
diff --git a/docs/plugin.md b/docs/plugin.md
index 8f695c9b..a3404e84 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -26,10 +26,11 @@ cd WindowTranslator.Plugin.YourPlugin
WindowTranslator.Plugin.YourPlugin
- WindowTranslator Your Plugin
+ Your Plugin
1.0.0
YourName
プラグインストアに表示する具体的な説明文
+ https://github.com/YourName/YourPlugin
$(PackageTags);windowtranslator-plugin
MIT
From 2bdf8300fab9edf0bddbd23931df43c539dbbdd5 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Mon, 10 Aug 2026 00:03:17 +0900
Subject: [PATCH 23/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=82=B9=E3=83=88=E3=82=A2=E3=81=AE=E3=82=A2=E3=82=A4?=
=?UTF-8?q?=E3=82=B3=E3=83=B3=E8=A1=A8=E7=A4=BA=E3=83=AC=E3=82=A4=E3=82=A2?=
=?UTF-8?q?=E3=82=A6=E3=83=88=E3=82=92=E5=88=B7=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Modules/PluginStore/PluginStoreView.xaml | 101 +++++++++---------
1 file changed, 50 insertions(+), 51 deletions(-)
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
index 3e9f4025..483e2357 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
@@ -68,7 +68,7 @@
-
+
@@ -78,59 +78,56 @@
-
-
-
-
-
-
-
-
-
-
-
-
+ Margin="0,0,8,0">
+
+
+
+
+
+
+
+
+
+
+
@@ -159,10 +156,12 @@
From 650b194712ff9b6e797c3b605e38870a7201bbdb Mon Sep 17 00:00:00 2001
From: Freesia
Date: Tue, 11 Aug 2026 16:17:20 +0900
Subject: [PATCH 24/43] =?UTF-8?q?=E3=83=AC=E3=82=A4=E3=82=A2=E3=82=A6?=
=?UTF-8?q?=E3=83=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Modules/PluginStore/PluginStoreView.xaml | 25 +++++++++++--------
.../Properties/Resources.Designer.cs | 22 +++++++++++-----
2 files changed, 30 insertions(+), 17 deletions(-)
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
index 483e2357..202e07b7 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
@@ -20,7 +20,6 @@
BasedOn="{StaticResource {x:Type ui:Button}}"
TargetType="ui:Button">
-
@@ -78,12 +77,10 @@
-
+
-
+
-
+
@@ -331,11 +334,11 @@
Grid.Column="2"
Command="{Binding DataContext.InstallCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
- Content="{x:Static properties:Resources.Install}"
DockPanel.Dock="Right"
Icon="{ui:SymbolIcon ArrowDownload24}"
IsEnabled="{Binding CanInstall}"
Style="{StaticResource InstallButtonStyle}"
+ ToolTip="{x:Static properties:Resources.Install}"
Visibility="{Binding IsNotInstalled, Converter={StaticResource b2vConv}}" />
@@ -344,11 +347,11 @@
Grid.Column="2"
Command="{Binding DataContext.InstallCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"
CommandParameter="{Binding}"
- Content="{x:Static properties:Resources.Update}"
DockPanel.Dock="Right"
Icon="{ui:SymbolIcon ArrowSync24}"
IsEnabled="{Binding CanInstall}"
Style="{StaticResource InstallButtonStyle}"
+ ToolTip="{x:Static properties:Resources.Update}"
Visibility="{Binding CanUpdate, Converter={StaticResource b2vConv}}" />
diff --git a/WindowTranslator/Properties/Resources.Designer.cs b/WindowTranslator/Properties/Resources.Designer.cs
index 3c016b28..b0c8adc7 100644
--- a/WindowTranslator/Properties/Resources.Designer.cs
+++ b/WindowTranslator/Properties/Resources.Designer.cs
@@ -367,6 +367,16 @@ internal Resources() {
///
public static string Misc => ResourceManager.GetString("Misc", resourceCulture) ?? string.Empty;
+ ///
+ /// "マウスポインター判定の余白" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string MousePointerHitTestPadding => ResourceManager.GetString("MousePointerHitTestPadding", resourceCulture) ?? string.Empty;
+
+ ///
+ /// "マウスポインター周辺の判定領域に追加するピクセル数" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string MousePointerHitTestPadding_Desc => ResourceManager.GetString("MousePointerHitTestPadding_Desc", resourceCulture) ?? string.Empty;
+
///
/// "すでにWindowTranslatorが起動中です" に類似しているローカライズされた文字列を検索します。
///
@@ -453,19 +463,19 @@ internal Resources() {
public static string PluginInstallSuccess => ResourceManager.GetString("PluginInstallSuccess", resourceCulture) ?? string.Empty;
///
- /// "プラグイン" に類似しているローカライズされた文字列を検索します。
+ /// "公式プラグイン" に類似しているローカライズされた文字列を検索します。
///
- public static string PluginStore => ResourceManager.GetString("PluginStore", resourceCulture) ?? string.Empty;
+ public static string PluginOfficial => ResourceManager.GetString("PluginOfficial", resourceCulture) ?? string.Empty;
///
- /// プラグインストアの免責事項に類似しているローカライズされた文字列を検索します。
+ /// "プラグイン" に類似しているローカライズされた文字列を検索します。
///
- public static string PluginStoreDisclaimer => ResourceManager.GetString("PluginStoreDisclaimer", resourceCulture) ?? string.Empty;
+ public static string PluginStore => ResourceManager.GetString("PluginStore", resourceCulture) ?? string.Empty;
///
- /// "公式プラグイン" に類似しているローカライズされた文字列を検索します。
+ /// "プラグインはNuGet上の第三者パッケージです。各パッケージのライセンスは所有者から付与され、Win..." に類似しているローカライズされた文字列を検索します。
///
- public static string PluginOfficial => ResourceManager.GetString("PluginOfficial", resourceCulture) ?? string.Empty;
+ public static string PluginStoreDisclaimer => ResourceManager.GetString("PluginStoreDisclaimer", resourceCulture) ?? string.Empty;
///
/// "プレリリース" に類似しているローカライズされた文字列を検索します。
From 44770a76a9bddd6ca16bdf4fcc09e8d79a254fea Mon Sep 17 00:00:00 2001
From: Freesia
Date: Tue, 11 Aug 2026 16:28:47 +0900
Subject: [PATCH 25/43] =?UTF-8?q?=E3=83=AC=E3=82=A4=E3=82=A2=E3=82=A6?=
=?UTF-8?q?=E3=83=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
WindowTranslator/Modules/PluginStore/PluginStoreView.xaml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
index 202e07b7..60a93c38 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreView.xaml
@@ -298,8 +298,10 @@
Visibility="{Binding HasPrereleaseVersion, Converter={StaticResource b2vConv}}" />
Date: Tue, 11 Aug 2026 16:33:10 +0900
Subject: [PATCH 26/43] =?UTF-8?q?PR=20#615=E3=81=AE=E3=83=AC=E3=83=93?=
=?UTF-8?q?=E3=83=A5=E3=83=BC=E6=8C=87=E6=91=98=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../NuGetPluginServiceTests.cs | 116 +++++++++++++++++-
.../PluginStore/NuGetPackageInstaller.cs | 16 ++-
.../Modules/PluginStore/NuGetPluginCatalog.cs | 24 +++-
.../Modules/PluginStore/NuGetPluginService.cs | 17 ++-
.../PluginStore/PluginCompatibility.cs | 17 +++
.../PluginStore/PluginStoreViewModel.cs | 18 ++-
WindowTranslator/Program.cs | 4 +-
docs/plugin.md | 2 +
8 files changed, 197 insertions(+), 17 deletions(-)
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index 64336489..c9514b68 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -439,7 +439,50 @@ await File.WriteAllTextAsync(
Assert.Empty(service.PackageSnapshot.InstalledPackages);
Assert.Empty(NuGetPluginCatalog.GetLoadablePackageIds(
testDirectory,
- hostMajorVersion: 7));
+ hostMajorVersion: 7,
+ hostAbstractionsVersion: NuGetVersion.Parse("1.0.0")));
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
+ [Fact]
+ public async Task ManifestWithoutAbstractionsVersionRangeIsRejected()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ Directory.CreateDirectory(Path.Combine(testDirectory, "Legacy.Plugin"));
+ await File.WriteAllTextAsync(
+ Path.Combine(testDirectory, "nuget-manifest.json"),
+ JsonSerializer.Serialize(new
+ {
+ Packages = new[]
+ {
+ new
+ {
+ Id = "Legacy.Plugin",
+ Version = "1.0.0",
+ HostMajorVersion = 7,
+ },
+ },
+ }));
+ using var handler = new InMemoryNuGetHandler();
+ using var service = CreateService(
+ handler,
+ testDirectory,
+ hostMajorVersion: 7);
+
+ await service.RefreshPackageInformationAsync();
+
+ Assert.IsType(service.PackageSnapshot.Error);
+ Assert.Empty(service.PackageSnapshot.InstalledPackages);
+ Assert.Empty(NuGetPluginCatalog.GetLoadablePackageIds(
+ testDirectory,
+ hostMajorVersion: 7,
+ hostAbstractionsVersion: NuGetVersion.Parse("1.0.0")));
}
finally
{
@@ -475,6 +518,51 @@ await File.WriteAllTextAsync(
}
}
+ [Fact]
+ public async Task InstalledPackageCompatibilityChecksAbstractionsVersionRange()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ await File.WriteAllTextAsync(
+ Path.Combine(testDirectory, "nuget-manifest.json"),
+ JsonSerializer.Serialize(new InstalledManifest(
+ [
+ new InstalledPackageInfo(
+ "Range.Plugin",
+ "1.0.0",
+ HostMajorVersion: 7,
+ AbstractionsVersionRange: "[2.0.0, 3.0.0)"),
+ ])));
+ using var handler = new InMemoryNuGetHandler();
+ var hostAbstractionsVersion = NuGetVersion.Parse("1.5.0");
+ using var service = CreateService(
+ handler,
+ testDirectory,
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ [NuGetPluginService.AbstractionsPackageId] = hostAbstractionsVersion,
+ },
+ hostMajorVersion: 7);
+
+ await service.RefreshPackageInformationAsync();
+ var package = Assert.Single(service.PackageSnapshot.InstalledPackages);
+ var loadablePackages = NuGetPluginCatalog.GetLoadablePackageIds(
+ testDirectory,
+ hostMajorVersion: 7,
+ hostAbstractionsVersion);
+
+ Assert.Equal(PluginCompatibility.ValidationDisabled, package.IsCompatible);
+ Assert.Equal(
+ PluginCompatibility.ValidationDisabled,
+ loadablePackages.Contains("Range.Plugin"));
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
[Fact]
public async Task BackgroundServiceRefreshesPluginInformationWithoutOpeningSettings()
{
@@ -789,6 +877,23 @@ public void PackageVersionSelectionRequiresOptInForPrerelease()
Assert.Equal("2.0.0-preview.1", prereleaseOnlyPackage.LatestVersion);
Assert.True(prereleaseOnlyPackage.CanInstall);
+
+ var releaseNewerThanPrerelease = new PluginPackageViewModel(
+ new NuGetPackageInfo(
+ "Released.Plugin",
+ "Released Plugin",
+ string.Empty,
+ string.Empty,
+ null,
+ null,
+ ["1.9.0-preview.1", "2.0.0"]),
+ isInstalled: false,
+ installedVersion: null)
+ {
+ UsePrerelease = true,
+ };
+
+ Assert.Equal("2.0.0", releaseNewerThanPrerelease.LatestVersion);
}
[Fact]
@@ -940,6 +1045,10 @@ public async Task InstallAcceptsCompatibleHostAbstractionsWithoutDownloadingIt()
await service.InstallPackageAsync("Root.Plugin", "1.0.0");
Assert.True(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+ Assert.Equal(
+ "[1.0.0, 2.0.0)",
+ Assert.Single(service.PackageSnapshot.InstalledPackages)
+ .AbstractionsVersionRange);
Assert.DoesNotContain(
handler.RequestedPaths,
path => path.Contains(
@@ -1427,7 +1536,8 @@ public void CatalogSynchronizationFollowsCompatibilityValidationSetting()
var loadablePackages = NuGetPluginCatalog.GetLoadablePackageIds(
sourceDirectory,
- hostMajorVersion: 7);
+ hostMajorVersion: 7,
+ hostAbstractionsVersion: NuGetVersion.Parse("1.0.0"));
NuGetPluginCatalog.SynchronizePluginFiles(
sourceDirectory,
destinationDirectory,
@@ -1560,6 +1670,7 @@ await NuGetPluginOperation.SaveManifestAsync(
sourceDirectory,
tempDirectory,
hostMajorVersion: 1,
+ hostAbstractionsVersion: NuGetVersion.Parse("1.0.0"),
options);
await catalog.Initialize();
@@ -1636,6 +1747,7 @@ await NuGetPluginOperation.SaveManifestAsync(
sourceDirectory,
tempDirectory,
hostMajorVersion: 1,
+ hostAbstractionsVersion: NuGetVersion.Parse("1.0.0"),
options);
await catalog.Initialize();
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
index e90bead4..6379213b 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
@@ -30,7 +30,7 @@ internal sealed class NuGetPackageInstaller(
private readonly ILogger logger = logger;
private readonly IReadOnlyDictionary hostPackageVersions = hostPackageVersions;
- public async Task InstallAsync(
+ public async Task InstallAsync(
string packageId,
string version,
string destinationDirectory,
@@ -63,6 +63,13 @@ public async Task InstallAsync(
destinationDirectory,
requirePluginAssembly: artifact.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase));
}
+
+ var rootPackage = artifacts.First(artifact =>
+ artifact.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase));
+ return rootPackage.Metadata.Dependencies.First(dependency =>
+ dependency.Id.Equals(
+ NuGetPluginService.AbstractionsPackageId,
+ StringComparison.OrdinalIgnoreCase)).VersionRange;
}
finally
{
@@ -218,7 +225,7 @@ await DownloadPackageAsync(
this.hostPackageVersions,
requirePluginPackage: currentId.Equals(rootPackageId, StringComparison.OrdinalIgnoreCase));
- artifacts[currentId] = new PackageArtifact(currentId, packagePath);
+ artifacts[currentId] = new PackageArtifact(currentId, packagePath, metadata);
foreach (var dependency in metadata.Dependencies.Where(IncludesRuntimeAssets))
{
if (this.hostPackageVersions.ContainsKey(dependency.Id))
@@ -586,7 +593,10 @@ private static void TryDeleteDirectory(string directory)
}
}
- private sealed record PackageArtifact(string Id, string PackagePath);
+ private sealed record PackageArtifact(
+ string Id,
+ string PackagePath,
+ PackageMetadata Metadata);
private sealed record DependencyConstraint(string Source, VersionRange Range);
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 161bdc77..365c125a 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -2,6 +2,7 @@
using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
+using NuGet.Versioning;
using Weikio.PluginFramework.Abstractions;
using Weikio.PluginFramework.Catalogs;
using Weikio.PluginFramework.Context;
@@ -20,14 +21,21 @@ public sealed class NuGetPluginCatalog : IPluginCatalog
private readonly string sourceDir;
private readonly string tempDir;
private readonly int hostMajorVersion;
+ private readonly NuGetVersion hostAbstractionsVersion;
private readonly FolderPluginCatalogOptions options;
private CompositePluginCatalog innerCatalog = new();
public NuGetPluginCatalog(
string sourceDir,
int hostMajorVersion,
+ NuGetVersion hostAbstractionsVersion,
FolderPluginCatalogOptions options)
- : this(sourceDir, DefaultTempDir, hostMajorVersion, options)
+ : this(
+ sourceDir,
+ DefaultTempDir,
+ hostMajorVersion,
+ hostAbstractionsVersion,
+ options)
{
}
@@ -35,11 +43,13 @@ internal NuGetPluginCatalog(
string sourceDir,
string tempDir,
int hostMajorVersion,
+ NuGetVersion hostAbstractionsVersion,
FolderPluginCatalogOptions options)
{
this.sourceDir = sourceDir;
this.tempDir = tempDir;
this.hostMajorVersion = hostMajorVersion;
+ this.hostAbstractionsVersion = hostAbstractionsVersion;
this.options = options;
}
@@ -54,7 +64,8 @@ public async Task Initialize()
.ConfigureAwait(false);
var loadablePackages = GetLoadablePackageIds(
this.sourceDir,
- this.hostMajorVersion);
+ this.hostMajorVersion,
+ this.hostAbstractionsVersion);
loadablePackages.ExceptWith(unresolvedOperations);
SynchronizePluginFiles(this.sourceDir, this.tempDir, loadablePackages);
@@ -343,7 +354,8 @@ internal static void SynchronizePluginFiles(
internal static HashSet GetLoadablePackageIds(
string sourceDirectory,
- int hostMajorVersion)
+ int hostMajorVersion,
+ NuGetVersion hostAbstractionsVersion)
{
try
{
@@ -355,9 +367,11 @@ internal static HashSet GetLoadablePackageIds(
NuGetPluginService.ManifestJsonOptions)?.Packages
?? throw new InvalidDataException("プラグインmanifestにパッケージ一覧がありません。");
return packages
- .Where(package => PluginCompatibility.IsHostMajorCompatible(
+ .Where(package => PluginCompatibility.IsInstalledPackageCompatible(
package.HostMajorVersion,
- hostMajorVersion))
+ hostMajorVersion,
+ package.AbstractionsVersionRange,
+ hostAbstractionsVersion))
.Select(package => package.Id)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 22c41019..09d1c1e0 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -258,7 +258,7 @@ public async Task InstallPackageAsync(string packageId, string version, IProgres
packageResource,
this.logger,
this.hostPackageVersions);
- await installer.InstallAsync(
+ var abstractionsVersionRange = await installer.InstallAsync(
packageId,
version,
pluginOperation.WorkingPath,
@@ -275,7 +275,11 @@ await installer.InstallAsync(
[
.. currentManifest.Packages.Where(package =>
!package.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)),
- new(packageId, version, this.hostMajorVersion),
+ new(
+ packageId,
+ version,
+ this.hostMajorVersion,
+ abstractionsVersionRange.ToString()),
]);
await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
pluginOperation.Commit();
@@ -402,9 +406,11 @@ private InstalledPackageInfo[] GetCompatibilityAwarePackages(
=> packages
.Select(package => package with
{
- IsCompatible = PluginCompatibility.IsHostMajorCompatible(
+ IsCompatible = PluginCompatibility.IsInstalledPackageCompatible(
package.HostMajorVersion,
- this.hostMajorVersion),
+ this.hostMajorVersion,
+ package.AbstractionsVersionRange,
+ this.hostPackageVersions.GetValueOrDefault(AbstractionsPackageId)),
})
.ToArray();
@@ -512,7 +518,8 @@ public record NuGetPackageInfo(
public record InstalledPackageInfo(
string Id,
string Version,
- [property: JsonRequired] int HostMajorVersion)
+ [property: JsonRequired] int HostMajorVersion,
+ [property: JsonRequired] string AbstractionsVersionRange = "(, )")
{
[JsonIgnore]
public bool IsCompatible { get; init; } = true;
diff --git a/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs b/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs
index cb1f72dc..084f75c6 100644
--- a/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginCompatibility.cs
@@ -17,4 +17,21 @@ internal static bool IsVersionCompatible(VersionRange? requiredVersion, NuGetVer
internal static bool IsHostMajorCompatible(int installedHostMajorVersion, int hostMajorVersion)
=> ValidationDisabled
|| installedHostMajorVersion == hostMajorVersion;
+
+ internal static bool IsInstalledPackageCompatible(
+ int installedHostMajorVersion,
+ int hostMajorVersion,
+ string? abstractionsVersionRange,
+ NuGetVersion? hostAbstractionsVersion)
+ {
+ if (ValidationDisabled)
+ {
+ return true;
+ }
+
+ return IsHostMajorCompatible(installedHostMajorVersion, hostMajorVersion)
+ && !string.IsNullOrWhiteSpace(abstractionsVersionRange)
+ && VersionRange.TryParse(abstractionsVersionRange, out var requiredVersion)
+ && IsVersionCompatible(requiredVersion, hostAbstractionsVersion);
+ }
}
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index 2a53d7a0..b6d454f4 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -400,7 +400,7 @@ public partial class PluginPackageViewModel : ObservableObject
public string? ReleaseVersion { get; }
public string? PrereleaseVersion { get; }
public string? LatestVersion => this.UsePrerelease
- ? this.PrereleaseVersion
+ ? GetLatestVersion(this.ReleaseVersion, this.PrereleaseVersion)
: this.ReleaseVersion;
public bool HasPrereleaseVersion => this.PrereleaseVersion is not null;
public bool CanInstall => !this.IsInstalling && this.LatestVersion is not null;
@@ -521,6 +521,22 @@ private void RefreshUpdateAvailable()
private static NuGetVersion? ParseVersion(string version)
=> NuGetVersion.TryParse(version, out var parsed) ? parsed : null;
+ private static string? GetLatestVersion(string? releaseVersion, string? prereleaseVersion)
+ {
+ if (releaseVersion is null)
+ {
+ return prereleaseVersion;
+ }
+ if (prereleaseVersion is null)
+ {
+ return releaseVersion;
+ }
+
+ return IsNewerVersion(prereleaseVersion, releaseVersion)
+ ? prereleaseVersion
+ : releaseVersion;
+ }
+
private static bool IsNewerVersion(string latestVersion, string installedVersion)
{
if (NuGetVersion.TryParse(latestVersion, out var latest)
diff --git a/WindowTranslator/Program.cs b/WindowTranslator/Program.cs
index addd1d80..8b27fdc3 100644
--- a/WindowTranslator/Program.cs
+++ b/WindowTranslator/Program.cs
@@ -117,9 +117,11 @@
var userPluginsDir = Path.Combine(PathUtility.UserDir, "plugins");
var nugetPluginsDir = Path.Combine(PathUtility.UserDir, "nuget-plugins");
+var hostPackageVersions = NuGetPluginService.CreateHostPackageVersions();
IPluginCatalog pluginFolderCatalog = new NuGetPluginCatalog(
nugetPluginsDir,
AppInfo.Instance.Version.Major,
+ hostPackageVersions[NuGetPluginService.AbstractionsPackageId],
new() { PluginNameOptions = { PluginNameGenerator = GetPluginName } });
var fallbackPluginCatalogs = new List();
var appPluginDir = @".\plugins";
@@ -190,7 +192,7 @@
sp.GetRequiredService(),
sp.GetRequiredService(),
nugetPluginsDir,
- NuGetPluginService.CreateHostPackageVersions(),
+ hostPackageVersions,
AppInfo.Instance.Version.Major))
.AddHostedService(sp => sp.GetRequiredService());
builder.Services.AddTransient();
diff --git a/docs/plugin.md b/docs/plugin.md
index a3404e84..12afad3a 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -144,6 +144,8 @@ NuGetパッケージで宣言されたランタイム依存関係も再帰的に
プラグイン情報は WindowTranslator の起動後にバックグラウンドで更新されます。
プラグインをインストールした WindowTranslator と現在のメジャーバージョンが異なる場合、
そのプラグインは互換性がないものとして起動時のロード対象から除外されます。
+また、インストール時に記録された `WindowTranslator.Abstractions` の依存バージョン範囲を
+現在のホストが満たさない場合も、起動時のロード対象から除外されます。
互換バージョンを再インストールすると、次回起動から再び利用できます。
アンインストールすると管理フォルダのパッケージは直ちに削除されます。
From c5d9e0ff3d4f7c390c43e0be6faaf2f57265dbb0 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Tue, 11 Aug 2026 17:13:25 +0900
Subject: [PATCH 27/43] =?UTF-8?q?=E6=97=A7manifest=E4=BA=92=E6=8F=9B?=
=?UTF-8?q?=E7=94=A8=E3=81=AE=E6=97=A2=E5=AE=9A=E5=80=A4=E3=82=92=E5=89=8A?=
=?UTF-8?q?=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../NuGetPluginServiceTests.cs | 120 ++++++++++--------
.../Modules/PluginStore/NuGetPluginService.cs | 2 +-
2 files changed, 66 insertions(+), 56 deletions(-)
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index c9514b68..cbce4c76 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -387,7 +387,11 @@ public async Task ManifestLoadsInstalledPackages()
await File.WriteAllTextAsync(
Path.Combine(testDirectory, "nuget-manifest.json"),
JsonSerializer.Serialize(new InstalledManifest(
- [new InstalledPackageInfo("Root.Plugin", "1.0.0", HostMajorVersion: 7)])));
+ [new InstalledPackageInfo(
+ "Root.Plugin",
+ "1.0.0",
+ HostMajorVersion: 7,
+ AbstractionsVersionRange: "[1.0.0, 2.0.0)")])));
using var handler = new InMemoryNuGetHandler();
using var service = CreateService(
@@ -448,48 +452,6 @@ await File.WriteAllTextAsync(
}
}
- [Fact]
- public async Task ManifestWithoutAbstractionsVersionRangeIsRejected()
- {
- var testDirectory = CreateTestDirectory();
- try
- {
- Directory.CreateDirectory(Path.Combine(testDirectory, "Legacy.Plugin"));
- await File.WriteAllTextAsync(
- Path.Combine(testDirectory, "nuget-manifest.json"),
- JsonSerializer.Serialize(new
- {
- Packages = new[]
- {
- new
- {
- Id = "Legacy.Plugin",
- Version = "1.0.0",
- HostMajorVersion = 7,
- },
- },
- }));
- using var handler = new InMemoryNuGetHandler();
- using var service = CreateService(
- handler,
- testDirectory,
- hostMajorVersion: 7);
-
- await service.RefreshPackageInformationAsync();
-
- Assert.IsType(service.PackageSnapshot.Error);
- Assert.Empty(service.PackageSnapshot.InstalledPackages);
- Assert.Empty(NuGetPluginCatalog.GetLoadablePackageIds(
- testDirectory,
- hostMajorVersion: 7,
- hostAbstractionsVersion: NuGetVersion.Parse("1.0.0")));
- }
- finally
- {
- DeleteTestDirectory(testDirectory);
- }
- }
-
[Fact]
public async Task InstalledPackageCompatibilityFollowsValidationSetting()
{
@@ -499,7 +461,11 @@ public async Task InstalledPackageCompatibilityFollowsValidationSetting()
await File.WriteAllTextAsync(
Path.Combine(testDirectory, "nuget-manifest.json"),
JsonSerializer.Serialize(new InstalledManifest(
- [new InstalledPackageInfo("Old.Plugin", "1.0.0", HostMajorVersion: 6)])));
+ [new InstalledPackageInfo(
+ "Old.Plugin",
+ "1.0.0",
+ HostMajorVersion: 6,
+ AbstractionsVersionRange: "[1.0.0, 2.0.0)")])));
using var handler = new InMemoryNuGetHandler();
using var service = CreateService(
handler,
@@ -622,7 +588,11 @@ public async Task PluginStoreKeepsInstalledPackagesVisibleWhenNuGetSearchFails()
await File.WriteAllTextAsync(
Path.Combine(testDirectory, "nuget-manifest.json"),
JsonSerializer.Serialize(new InstalledManifest(
- [new InstalledPackageInfo("Installed.Plugin", "1.2.3", HostMajorVersion: 7)])),
+ [new InstalledPackageInfo(
+ "Installed.Plugin",
+ "1.2.3",
+ HostMajorVersion: 7,
+ AbstractionsVersionRange: "(, )")])),
Encoding.UTF8);
using var handler = new InMemoryNuGetHandler();
handler.SearchException = new HttpRequestException("NuGet search failed.");
@@ -1107,7 +1077,11 @@ public async Task InstallRejectsPackageWithoutPluginTag()
File.WriteAllText(Path.Combine(targetDirectory, "plugin.txt"), "old");
await NuGetPluginOperation.SaveManifestAsync(
Path.Combine(testDirectory, "nuget-manifest.json"),
- new([new("Root.Plugin", "0.9.0", HostMajorVersion: 1)]),
+ new([new(
+ "Root.Plugin",
+ "0.9.0",
+ HostMajorVersion: 1,
+ AbstractionsVersionRange: "(, )")]),
CancellationToken.None);
using var handler = new InMemoryNuGetHandler();
handler.AddPackage(
@@ -1369,9 +1343,17 @@ public async Task InterruptedInstallIsRolledBackBeforeItIsTreatedAsCompleted()
{
const string packageId = "Root.Plugin";
var originalManifest = new InstalledManifest(
- [new InstalledPackageInfo(packageId, "1.0.0", HostMajorVersion: 1)]);
+ [new InstalledPackageInfo(
+ packageId,
+ "1.0.0",
+ HostMajorVersion: 1,
+ AbstractionsVersionRange: "(, )")]);
var updatedManifest = new InstalledManifest(
- [new InstalledPackageInfo(packageId, "2.0.0", HostMajorVersion: 1)]);
+ [new InstalledPackageInfo(
+ packageId,
+ "2.0.0",
+ HostMajorVersion: 1,
+ AbstractionsVersionRange: "(, )")]);
var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
await NuGetPluginOperation.SaveManifestAsync(
manifestPath,
@@ -1419,9 +1401,17 @@ public async Task CompletedInstallKeepsNewFilesAndOnlyCleansOperationData()
{
const string packageId = "Root.Plugin";
var originalManifest = new InstalledManifest(
- [new InstalledPackageInfo(packageId, "1.0.0", HostMajorVersion: 1)]);
+ [new InstalledPackageInfo(
+ packageId,
+ "1.0.0",
+ HostMajorVersion: 1,
+ AbstractionsVersionRange: "(, )")]);
var updatedManifest = new InstalledManifest(
- [new InstalledPackageInfo(packageId, "2.0.0", HostMajorVersion: 1)]);
+ [new InstalledPackageInfo(
+ packageId,
+ "2.0.0",
+ HostMajorVersion: 1,
+ AbstractionsVersionRange: "(, )")]);
var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
await NuGetPluginOperation.SaveManifestAsync(
manifestPath,
@@ -1470,7 +1460,11 @@ public async Task CompletedInstallRemainsLoadableWhenOperationCleanupFails()
{
const string packageId = "Root.Plugin";
var manifest = new InstalledManifest(
- [new InstalledPackageInfo(packageId, "2.0.0", HostMajorVersion: 1)]);
+ [new InstalledPackageInfo(
+ packageId,
+ "2.0.0",
+ HostMajorVersion: 1,
+ AbstractionsVersionRange: "(, )")]);
var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
var operation = await NuGetPluginOperation.BeginAsync(
sourceDirectory,
@@ -1530,8 +1524,16 @@ public void CatalogSynchronizationFollowsCompatibilityValidationSetting()
Path.Combine(sourceDirectory, "nuget-manifest.json"),
JsonSerializer.Serialize(new InstalledManifest(
[
- new InstalledPackageInfo("Compatible.Plugin", "1.0.0", HostMajorVersion: 7),
- new InstalledPackageInfo("Incompatible.Plugin", "1.0.0", HostMajorVersion: 6),
+ new InstalledPackageInfo(
+ "Compatible.Plugin",
+ "1.0.0",
+ HostMajorVersion: 7,
+ AbstractionsVersionRange: "(, )"),
+ new InstalledPackageInfo(
+ "Incompatible.Plugin",
+ "1.0.0",
+ HostMajorVersion: 6,
+ AbstractionsVersionRange: "(, )"),
])));
var loadablePackages = NuGetPluginCatalog.GetLoadablePackageIds(
@@ -1643,7 +1645,11 @@ public async Task CatalogLoadsARealAssemblyFromAPackageSubdirectory()
SearchOption.AllDirectories));
await NuGetPluginOperation.SaveManifestAsync(
Path.Combine(sourceDirectory, "nuget-manifest.json"),
- new([new("Catalog.Probe", "1.0.0", HostMajorVersion: 1)]),
+ new([new(
+ "Catalog.Probe",
+ "1.0.0",
+ HostMajorVersion: 1,
+ AbstractionsVersionRange: "(, )")]),
CancellationToken.None);
var options = new FolderPluginCatalogOptions();
@@ -1719,7 +1725,11 @@ public async Task CatalogLoadsTheSatelliteAssemblyForTheRequestedCulture()
}
await NuGetPluginOperation.SaveManifestAsync(
Path.Combine(sourceDirectory, "nuget-manifest.json"),
- new([new("Catalog.Probe", "1.0.0", HostMajorVersion: 1)]),
+ new([new(
+ "Catalog.Probe",
+ "1.0.0",
+ HostMajorVersion: 1,
+ AbstractionsVersionRange: "(, )")]),
CancellationToken.None);
var options = new FolderPluginCatalogOptions();
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 09d1c1e0..ac0789dc 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -519,7 +519,7 @@ public record InstalledPackageInfo(
string Id,
string Version,
[property: JsonRequired] int HostMajorVersion,
- [property: JsonRequired] string AbstractionsVersionRange = "(, )")
+ [property: JsonRequired] string AbstractionsVersionRange)
{
[JsonIgnore]
public bool IsCompatible { get; init; } = true;
From 18b57629aaeeb00890e133f9f03c607058ca7a6c Mon Sep 17 00:00:00 2001
From: Freesia
Date: Tue, 11 Aug 2026 19:23:34 +0900
Subject: [PATCH 28/43] =?UTF-8?q?NuGet=E6=89=80=E6=9C=89=E8=80=85=E3=81=A7?=
=?UTF-8?q?=E5=85=AC=E5=BC=8F=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4=E3=83=B3?=
=?UTF-8?q?=E3=82=92=E5=88=A4=E5=AE=9A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../NuGetPluginServiceTests.cs | 60 +++++++++++++++++--
.../Modules/PluginStore/NuGetPluginService.cs | 9 ++-
.../PluginStore/PluginStoreViewModel.cs | 4 +-
3 files changed, 64 insertions(+), 9 deletions(-)
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index cbce4c76..48d3f49b 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -746,6 +746,55 @@ public async Task SearchReturnsReleaseAndPrereleaseVersions()
}
}
+ [Fact]
+ public async Task SearchUsesNuGetOwnersForOfficialPackageStatus()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.SearchResults =
+ [
+ CreatePackageSearchMetadata(
+ "Official.Plugin",
+ title: null,
+ description: null,
+ authors: "Other",
+ projectUrl: null,
+ licenseUrl: null,
+ owners: [NuGetPluginService.OfficialPackageOwner]),
+ CreatePackageSearchMetadata(
+ "Spoofed.Plugin",
+ title: null,
+ description: null,
+ authors: NuGetPluginService.OfficialPackageOwner,
+ projectUrl: null,
+ licenseUrl: null,
+ owners: ["Other"]),
+ ];
+ handler.AddMetadataVersions(
+ "Official.Plugin",
+ CreatePluginVersionMetadata("1.0.0"));
+ handler.AddMetadataVersions(
+ "Spoofed.Plugin",
+ CreatePluginVersionMetadata("1.0.0"));
+ using var service = CreateService(handler, testDirectory);
+
+ await service.RefreshPackageInformationAsync();
+
+ Assert.True(service.PackageSnapshot.Packages
+ .Single(package => package.Id == "Official.Plugin")
+ .IsOfficial);
+ Assert.False(service.PackageSnapshot.Packages
+ .Single(package => package.Id == "Spoofed.Plugin")
+ .IsOfficial);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
[Fact]
public async Task SearchKeepsOnlyVersionsWithCompatibleDirectAbstractionsDependency()
{
@@ -867,18 +916,19 @@ public void PackageVersionSelectionRequiresOptInForPrerelease()
}
[Fact]
- public void PackagePresentationUsesMetadataAndMarksFreesiaAsOfficial()
+ public void PackagePresentationUsesOfficialFlagFromMetadata()
{
var package = new PluginPackageViewModel(
new NuGetPackageInfo(
"Test.Plugin",
"Test Plugin",
"Description",
- "Other; Freesia",
+ "Other",
null,
null,
["1.0.0"],
- "https://nuget.test/icons/test-plugin.png"),
+ "https://nuget.test/icons/test-plugin.png",
+ IsOfficial: true),
isInstalled: false,
installedVersion: null);
@@ -1851,7 +1901,8 @@ private static IPackageSearchMetadata CreatePackageSearchMetadata(
IEnumerable? dependencySets = null,
bool isListed = true,
string? readmeFileUrl = null,
- string? iconUrl = null)
+ string? iconUrl = null,
+ IReadOnlyList? owners = null)
=> new TestPackageSearchMetadata
{
Identity = new PackageIdentity(
@@ -1866,6 +1917,7 @@ private static IPackageSearchMetadata CreatePackageSearchMetadata(
IsListed = isListed,
ReadmeFileUrl = readmeFileUrl!,
IconUrl = iconUrl is null ? null! : new Uri(iconUrl),
+ OwnersList = owners ?? [],
};
private static async Task WaitForReadmeAsync(
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index ac0789dc..1f231b7e 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -23,6 +23,7 @@ public sealed class NuGetPluginService : BackgroundService
internal const string HttpClientName = "NuGetPluginReadme";
internal const string PluginTag = "windowtranslator-plugin";
internal const string AbstractionsPackageId = "WindowTranslator.Abstractions";
+ internal const string OfficialPackageOwner = "Freesia";
private const int SearchResultLimit = 100;
private const int MaxConcurrentMetadataRequests = 8;
private static readonly TimeSpan PackageInformationRefreshInterval = TimeSpan.FromHours(1);
@@ -355,7 +356,10 @@ public async Task UninstallPackageAsync(string packageId, CancellationToken canc
Versions: compatibleVersions
.Select(version => version.Identity.Version.ToNormalizedString())
.ToArray(),
- IconUrl: data.IconUrl?.AbsoluteUri);
+ IconUrl: data.IconUrl?.AbsoluteUri,
+ IsOfficial: data.OwnersList.Contains(
+ OfficialPackageOwner,
+ StringComparer.OrdinalIgnoreCase));
}
private bool HasCompatibleAbstractionsDependency(
@@ -511,7 +515,8 @@ public record NuGetPackageInfo(
string? ProjectUrl,
string? LicenseUrl,
IReadOnlyList Versions,
- string? IconUrl = null
+ string? IconUrl = null,
+ bool IsOfficial = false
);
/// インストール済みパッケージ情報
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index b6d454f4..25d1ce77 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -477,9 +477,7 @@ public PluginPackageViewModel(
this.Description = info.Description;
this.Authors = info.Authors;
this.IconUrl = info.IconUrl;
- this.IsOfficial = info.Authors
- .Split([',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
- .Contains("Freesia", StringComparer.OrdinalIgnoreCase);
+ this.IsOfficial = info.IsOfficial;
this.ReleaseVersion = versions
.Where(version => !version.Parsed!.IsPrerelease)
.OrderByDescending(version => version.Parsed)
From b6b3df1e308c998eba3e9178b8dc678fc9b46f2b Mon Sep 17 00:00:00 2001
From: Freesia
Date: Tue, 11 Aug 2026 22:04:56 +0900
Subject: [PATCH 29/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=81=AE=E8=B5=B7=E5=8B=95=E6=99=82=E5=89=8A=E9=99=A4?=
=?UTF-8?q?=E3=81=A8NuGet=E5=85=AC=E9=96=8B=E6=96=B9=E5=BC=8F=E3=82=92?=
=?UTF-8?q?=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/dotnet-package.yml | 29 ++++++--
.../NuGetPluginServiceTests.cs | 58 +++++++++++++++-
.../Modules/PluginStore/NuGetPluginCatalog.cs | 67 +++++++++++++++----
.../Modules/PluginStore/NuGetPluginService.cs | 12 +---
docs/plugin.md | 5 +-
5 files changed, 140 insertions(+), 31 deletions(-)
diff --git a/.github/workflows/dotnet-package.yml b/.github/workflows/dotnet-package.yml
index b2ad6bf6..de573cbd 100644
--- a/.github/workflows/dotnet-package.yml
+++ b/.github/workflows/dotnet-package.yml
@@ -1,12 +1,16 @@
name: .NET Core Package
on:
+ pull_request:
push:
tags: [v*]
jobs:
build:
runs-on: windows-latest
+ permissions:
+ contents: read
+ id-token: write
steps:
- uses: actions/checkout@v7
with:
@@ -28,12 +32,17 @@ jobs:
- id: package-version
shell: pwsh
run: |
- $tag = '${{ github.ref_name }}'
- if ($tag -notmatch '^v(?[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)$') {
- Write-Error "NuGet package tag must be v-prefixed SemVer: $tag"
- exit 1
+ if ('${{ github.event_name }}' -eq 'push') {
+ $tag = '${{ github.ref_name }}'
+ if ($tag -notmatch '^v(?[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)$') {
+ Write-Error "NuGet package tag must be v-prefixed SemVer: $tag"
+ exit 1
+ }
+ "version=$($Matches.version)" >> $env:GITHUB_OUTPUT
+ }
+ else {
+ "version=0.0.0-pr.${{ github.run_number }}" >> $env:GITHUB_OUTPUT
}
- "version=$($Matches.version)" >> $env:GITHUB_OUTPUT
- uses: Jimver/cuda-toolkit@v0.2.30
with:
cuda: '12.9.0'
@@ -76,4 +85,12 @@ jobs:
exit $LASTEXITCODE
}
}
- dotnet nuget push pack\*.nupkg -k ${{ secrets.NUGET_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate
+ - name: NuGet login
+ if: ${{ startsWith(github.ref, 'refs/tags/v') }}
+ id: nuget_login
+ uses: NuGet/login@v1
+ with:
+ user: Freesia
+ - name: NuGet push
+ if: ${{ startsWith(github.ref, 'refs/tags/v') }}
+ run: dotnet nuget push pack\*.nupkg --api-key ${{ steps.nuget_login.outputs.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index 48d3f49b..54ae0b60 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -188,7 +188,7 @@ await File.ReadAllTextAsync(
}
[Fact]
- public async Task UninstallRemovesManagedFilesImmediatelyAndAllowsManualReinstall()
+ public async Task UninstallDeletesManagedFilesAtNextStartupAndAllowsManualReinstall()
{
var testDirectory = CreateTestDirectory();
try
@@ -221,9 +221,12 @@ public async Task UninstallRemovesManagedFilesImmediatelyAndAllowsManualReinstal
await service.InstallPackageAsync("Root.Plugin", "1.0.0");
await service.UninstallPackageAsync("Root.Plugin");
- Assert.False(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+ Assert.True(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
Assert.Empty(service.PackageSnapshot.InstalledPackages);
+ NuGetPluginCatalog.DeleteUninstalledPackageDirectories(testDirectory);
+ Assert.False(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
+
await service.InstallPackageAsync("Root.Plugin", "2.0.0");
Assert.True(Directory.Exists(Path.Combine(testDirectory, "Root.Plugin")));
var installed = Assert.Single(service.PackageSnapshot.InstalledPackages);
@@ -1246,6 +1249,57 @@ public async Task SelectedPackageLoadsReadmeForTheSelectedReleaseChannel()
}
}
+ [Fact]
+ public void StartupCleanupDeletesOnlyPackagesMissingFromAReadableManifest()
+ {
+ var sourceDirectory = CreateTestDirectory();
+ try
+ {
+ var installedDirectory = Path.Combine(sourceDirectory, "Installed.Plugin");
+ var removedDirectory = Path.Combine(sourceDirectory, "Removed.Plugin");
+ var operationsDirectory = Path.Combine(
+ sourceDirectory,
+ NuGetPluginOperation.OperationsDirectoryName);
+ Directory.CreateDirectory(installedDirectory);
+ Directory.CreateDirectory(removedDirectory);
+ Directory.CreateDirectory(operationsDirectory);
+ File.WriteAllText(
+ Path.Combine(sourceDirectory, "nuget-manifest.json"),
+ JsonSerializer.Serialize(
+ new InstalledManifest(
+ [
+ new(
+ "Installed.Plugin",
+ "1.0.0",
+ HostMajorVersion: 1,
+ AbstractionsVersionRange: "(, )"),
+ ]),
+ NuGetPluginService.ManifestJsonOptions));
+
+ NuGetPluginCatalog.DeleteUninstalledPackageDirectories(sourceDirectory);
+
+ Assert.True(Directory.Exists(installedDirectory));
+ Assert.False(Directory.Exists(removedDirectory));
+ Assert.True(Directory.Exists(operationsDirectory));
+
+ var directoryKeptForInvalidManifest = Path.Combine(
+ sourceDirectory,
+ "Kept.For.Invalid.Manifest");
+ Directory.CreateDirectory(directoryKeptForInvalidManifest);
+ File.WriteAllText(
+ Path.Combine(sourceDirectory, "nuget-manifest.json"),
+ "{ invalid json");
+
+ NuGetPluginCatalog.DeleteUninstalledPackageDirectories(sourceDirectory);
+
+ Assert.True(Directory.Exists(directoryKeptForInvalidManifest));
+ }
+ finally
+ {
+ DeleteTestDirectory(sourceDirectory);
+ }
+ }
+
[Fact]
public void CatalogSynchronizationCopiesOnlyChangesAndRemovesStaleFiles()
{
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 365c125a..4c37a1e5 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
@@ -62,6 +63,7 @@ public async Task Initialize()
var unresolvedOperations = await NuGetPluginOperation
.RecoverInterruptedOperationsAsync(this.sourceDir)
.ConfigureAwait(false);
+ DeleteUninstalledPackageDirectories(this.sourceDir);
var loadablePackages = GetLoadablePackageIds(
this.sourceDir,
this.hostMajorVersion,
@@ -356,31 +358,72 @@ internal static HashSet GetLoadablePackageIds(
string sourceDirectory,
int hostMajorVersion,
NuGetVersion hostAbstractionsVersion)
+ => (TryLoadManifest(sourceDirectory)?.Packages ?? [])
+ .Where(package => PluginCompatibility.IsInstalledPackageCompatible(
+ package.HostMajorVersion,
+ hostMajorVersion,
+ package.AbstractionsVersionRange,
+ hostAbstractionsVersion))
+ .Select(package => package.Id)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ internal static void DeleteUninstalledPackageDirectories(string sourceDirectory)
+ {
+ var manifest = TryLoadManifest(sourceDirectory);
+ if (manifest is null || !Directory.Exists(sourceDirectory))
+ {
+ return;
+ }
+
+ var installedPackageIds = manifest.Packages
+ .Select(package => package.Id)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var packageDirectory in Directory.EnumerateDirectories(sourceDirectory))
+ {
+ var directoryName = Path.GetFileName(packageDirectory);
+ if (directoryName.Equals(
+ NuGetPluginOperation.OperationsDirectoryName,
+ StringComparison.OrdinalIgnoreCase)
+ || installedPackageIds.Contains(directoryName))
+ {
+ continue;
+ }
+
+ try
+ {
+ Directory.Delete(packageDirectory, recursive: true);
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
+ {
+ Trace.TraceWarning(
+ "アンインストール済みNuGetプラグインの削除に失敗しました: {0} ({1})",
+ packageDirectory,
+ ex);
+ }
+ }
+ }
+
+ private static InstalledManifest? TryLoadManifest(string sourceDirectory)
{
try
{
using var stream = File.OpenRead(Path.Combine(
sourceDirectory,
"nuget-manifest.json"));
- var packages = JsonSerializer.Deserialize(
+ var manifest = JsonSerializer.Deserialize(
stream,
- NuGetPluginService.ManifestJsonOptions)?.Packages
- ?? throw new InvalidDataException("プラグインmanifestにパッケージ一覧がありません。");
- return packages
- .Where(package => PluginCompatibility.IsInstalledPackageCompatible(
- package.HostMajorVersion,
- hostMajorVersion,
- package.AbstractionsVersionRange,
- hostAbstractionsVersion))
- .Select(package => package.Id)
- .ToHashSet(StringComparer.OrdinalIgnoreCase);
+ NuGetPluginService.ManifestJsonOptions)
+ ?? throw new InvalidDataException("プラグインmanifestが空です。");
+ return manifest.Packages is null
+ ? throw new InvalidDataException("プラグインmanifestにパッケージ一覧がありません。")
+ : manifest;
}
catch (Exception ex) when (ex is IOException
or UnauthorizedAccessException
or InvalidDataException
or JsonException)
{
- return new HashSet(StringComparer.OrdinalIgnoreCase);
+ return null;
}
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 1f231b7e..45db7eba 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -294,8 +294,8 @@ .. currentManifest.Packages.Where(package =>
}
///
- /// 指定したパッケージを管理フォルダから削除します。
- /// 実行中のプラグインは一時フォルダから読み込まれているため、反映には再起動が必要です。
+ /// 指定したパッケージをアンインストール対象としてmanifestから削除します。
+ /// 管理フォルダの実体は次回起動時に削除されます。
///
public async Task UninstallPackageAsync(string packageId, CancellationToken cancellationToken = default)
{
@@ -307,14 +307,8 @@ public async Task UninstallPackageAsync(string packageId, CancellationToken canc
await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
UpdateInstalledPackages(updatedManifest.Packages);
- var targetPath = NuGetPluginOperation.GetPackageDirectory(this.nugetPluginsDir, packageId);
- if (Directory.Exists(targetPath))
- {
- Directory.Delete(targetPath, recursive: true);
- }
-
this.logger.LogInformation(
- "パッケージ {PackageId} を管理フォルダからアンインストールしました。再起動後に反映されます。",
+ "パッケージ {PackageId} をアンインストール対象として記録しました。管理フォルダは次回起動時に削除されます。",
packageId);
}
diff --git a/docs/plugin.md b/docs/plugin.md
index 12afad3a..7fccac4a 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -148,8 +148,9 @@ NuGetパッケージで宣言されたランタイム依存関係も再帰的に
現在のホストが満たさない場合も、起動時のロード対象から除外されます。
互換バージョンを再インストールすると、次回起動から再び利用できます。
-アンインストールすると管理フォルダのパッケージは直ちに削除されます。
-実行中に読み込まれたプラグインを停止するには、WindowTranslator の再起動が必要です。
+アンインストールするとパッケージは読み込み対象から除外され、管理フォルダの実体は
+次回の WindowTranslator 起動時に削除されます。実行中に読み込まれたプラグインを
+停止するにも、WindowTranslator の再起動が必要です。
## 注意事項
From eb941bb83bab40bb8eb1d0a918c508349b1d8a88 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Tue, 11 Aug 2026 23:16:07 +0900
Subject: [PATCH 30/43] =?UTF-8?q?PLaMo=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=81=AENuGet=E3=83=91=E3=83=83=E3=82=B1=E3=83=BC?=
=?UTF-8?q?=E3=82=B8=E4=BD=9C=E6=88=90=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../WindowTranslator.Plugin.PLaMoPlugin.csproj | 1 -
1 file changed, 1 deletion(-)
diff --git a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
index d5790d8a..7c88752d 100644
--- a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
@@ -5,7 +5,6 @@
PLaMo Translator Plugin
Local PLaMo translation for WindowTranslator using LLamaSharp and CUDA.
true
- false
true
x64
From 9299077d8d0320641e5620fe1dc67f60cfde2361 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Thu, 13 Aug 2026 16:43:26 +0900
Subject: [PATCH 31/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3README=E3=82=92=E3=82=A2=E3=83=97=E3=83=AA=E8=A8=80?=
=?UTF-8?q?=E8=AA=9E=E3=81=AB=E5=AF=BE=E5=BF=9C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../README.md | 243 +++++++++++++++++-
.../README.md | 218 +++++++++++++++-
.../README.md | 220 +++++++++++++++-
.../README.md | 204 ++++++++++++++-
.../README.md | 183 ++++++++++++-
.../README.md | 205 ++++++++++++++-
.../README.md | 200 +++++++++++++-
.../README.md | 223 +++++++++++++++-
.../README.md | 189 +++++++++++++-
.../README.md | 217 +++++++++++++++-
.../README.md | 185 ++++++++++++-
.../LocalizedReadmeSelectorTests.cs | 137 ++++++++++
.../NuGetPluginServiceTests.cs | 49 ++++
.../PluginStore/LocalizedReadmeSelector.cs | 97 +++++++
.../Modules/PluginStore/NuGetPluginService.cs | 12 +-
.../PluginStore/PluginStoreViewModel.cs | 2 +
docs/plugin.md | 45 ++++
17 files changed, 2571 insertions(+), 58 deletions(-)
create mode 100644 WindowTranslator.Tests/LocalizedReadmeSelectorTests.cs
create mode 100644 WindowTranslator/Modules/PluginStore/LocalizedReadmeSelector.cs
diff --git a/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/README.md b/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/README.md
index 13c70698..ef3e498a 100644
--- a/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/README.md
@@ -1,12 +1,15 @@
# WindowTranslator Bergamot Translator Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) で、Bergamotによるニューラル機械翻訳をローカル実行するプラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)で、Bergamotによるニューラル機械翻訳をローカル実行するプラグインです。
## 機能
- 翻訳テキストを外部サービスへ送信せず、オフラインで翻訳
- 対応する言語ペアのモデルを初回利用時に自動取得
- 直接変換できない言語ペアでは、利用可能な場合に英語を経由して翻訳
+- ネットワーク状況や翻訳回数の上限に影響されないローカル処理
## 必要条件
@@ -15,8 +18,240 @@
モデルの取得後はオフラインで利用できます。モデルが提供されていない言語ペアでは、このモジュールを選択できません。
-## インストール
+## en
+
+A [WindowTranslator](https://github.com/Freeesia/WindowTranslator) plugin that runs Bergamot neural machine translation locally.
+
+## Features
+
+- Translates offline without sending text to an external service
+- Downloads the model for a supported language pair when it is first used
+- Translates through English when a direct model is unavailable and a suitable route exists
+- Runs locally without network instability or translation quotas
+
+## Requirements
+
+- A language pair for which a compatible Bergamot model is available
+- An internet connection when downloading a model for the first time
+
+After a model has been downloaded, translation can run offline. The module cannot be selected for language pairs that have no available model.
+
+## ar
+
+وحدة ترجمة آلية تعمل دون اتصال.
+
+### المزايا
+- **مجاني تماماً**: بدون أي رسوم
+- **بدون حد للترجمة**: ترجم بقدر ما تريد
+- **سريع**: المعالجة المحلية للترجمة السريعة
+- **الخصوصية**: لا حاجة لاتصال بالإنترنت، البيانات لا تُرسل للخارج
+- **الاستقرار**: لا يتأثر بالشبكة
+
+## cs
+
+Modul strojového překladu fungující offline.
+
+### Výhody
+- **Zcela zdarma**: Bez jakýchkoli poplatků
+- **Bez omezení překladu**: Můžete překládat kolikrát chcete
+- **Rychlý**: Překlad je rychlý, protože se zpracovává lokálně
+- **Soukromí**: Nevyžaduje připojení k internetu, data se neodesílají ven
+- **Stabilita**: Není ovlivněn síťovými podmínkami
+
+## de
+
+Ein Maschinenübersetzungsmodul, das offline funktioniert.
+
+### Vorteile
+- **Völlig kostenlos**: Keinerlei Kosten
+- **Keine Übersetzungslimits**: Sie können beliebig oft übersetzen
+- **Schnell**: Übersetzung ist schnell, da sie lokal verarbeitet wird
+- **Datenschutz**: Keine Internetverbindung erforderlich, Daten werden nicht extern gesendet
+- **Stabilität**: Nicht von Netzwerkbedingungen betroffen
+
+## es
+
+Un módulo de traducción automática que funciona sin conexión.
+
+### Ventajas
+- **Completamente gratis**: Sin cargos
+- **Sin límite de traducción**: Traduzca cuanto quiera
+- **Rápido**: Procesamiento local para traducción rápida
+- **Privacidad**: No se necesita conexión a Internet, los datos no se envían al exterior
+- **Estabilidad**: No afectado por la red
+
+## fa
+
+ماژول ترجمه ماشینی که بدون اتصال به اینترنت کار میکند.
+
+### مزایا
+- **کاملاً رایگان**: بدون هیچ هزینهای
+- **بدون محدودیت ترجمه**: هر مقدار که بخواهید ترجمه کنید
+- **سریع**: پردازش محلی برای ترجمه سریع
+- **حریم خصوصی**: نیاز به اتصال به اینترنت ندارد، دادهها به خارج ارسال نمیشوند
+- **پایداری**: تحت تأثیر شبکه نیست
+
+## fil
+
+Isang modyul ng machine translation na gumagana offline.
+
+### Mga Bentahe
+- **Lubos na Libre**: Walang anumang bayad
+- **Walang Limitasyon sa Pagsasalin**: Maaari kang magsalin ng maraming beses hangga't gusto mo
+- **Mabilis**: Ang pagsasalin ay mabilis dahil ito ay naproseso nang lokal
+- **Privacy**: Walang kailangang koneksyon sa internet, ang data ay hindi ipinapadala sa labas
+- **Katatagan**: Hindi apektado ng mga kondisyon ng network
+
+## fr
+
+Un module de traduction automatique qui fonctionne hors ligne.
+
+### Avantages
+- **Complètement gratuit**: Aucun frais
+- **Pas de limite de traduction**: Traduisez autant que vous voulez
+- **Rapide**: Traitement local pour une traduction rapide
+- **Confidentialité**: Pas de connexion Internet nécessaire, les données ne sont pas envoyées à l'extérieur
+- **Stabilité**: Non affecté par le réseau
+
+## hi
+
+ऑफ़लाइन काम करने वाला मशीन अनुवाद मॉड्यूल।
+
+### फायदे
+- **पूर्ण रूप से निःशुल्क**: बिल्कुल कोई शुल्क नहीं लगता
+- **अनुवाद की कोई सीमा नहीं**: कितनी भी बार अनुवाद कर सकते हैं
+- **तेज़**: स्थानीय प्रसंस्करण के कारण अनुवाद तेज़ है
+- **गोपनीयता**: इंटरनेट कनेक्शन की आवश्यकता नहीं, डेटा बाहर नहीं भेजा जाता
+- **स्थिरता**: नेटवर्क के प्रभाव से मुक्त
+
+## hu
+
+Offline gépi fordítási modul.
+
+### Előnyök
+- **Teljesen ingyenes**: Semmiféle díj
+- **Korlátlan fordítás**: Annyiszor fordíthat, amennyiszer szeretne
+- **Gyors**: Helyileg feldolgozott, gyors fordítás
+- **Adatvédelem**: Nem igényel internetkapcsolatot, adatokat nem küldi el
+- **Stabilitás**: Nincs hatással a hálózati feltételek
+
+## id
+
+Modul terjemahan mesin yang berfungsi offline.
+
+### Keuntungan
+- **Sepenuhnya Gratis**: Tidak ada biaya sama sekali
+- **Tidak Ada Batas Terjemahan**: Anda dapat menerjemahkan sebanyak yang Anda inginkan
+- **Cepat**: Terjemahan cepat karena diproses secara lokal
+- **Privasi**: Tidak ada koneksi internet yang diperlukan, data tidak dikirim ke luar
+- **Stabilitas**: Tidak terpengaruh oleh kondisi jaringan
+
+## ko
+
+오프라인에서 동작하는 기계 번역 모듈입니다.
+
+### 장점
+- **완전 무료**: 비용이 전혀 들지 않습니다
+- **번역 제한 없음**: 몇 번이라도 번역할 수 있습니다
+- **빠름**: 로컬에서 처리되므로 번역이 빠릅니다
+- **프라이버시**: 인터넷 연결이 불필요하며, 데이터가 외부로 전송되지 않습니다
+- **안정성**: 네트워크의 영향을 받지 않습니다
+
+## ms
+
+Modul terjemahan mesin yang berfungsi di luar talian.
+
+### Kelebihan
+- **Percuma Sepenuhnya**: Tiada caj sama sekali
+- **Tiada Had Terjemahan**: Anda boleh menterjemah sebanyak yang anda mahu
+- **Pantas**: Terjemahan pantas kerana diproses secara tempatan
+- **Privasi**: Tiada sambungan internet diperlukan, data tidak dihantar ke luar
+- **Kestabilan**: Tidak terjejas oleh keadaan rangkaian
+
+## pl
+
+Moduł tłumaczenia maszynowego działający offline.
+
+### Zalety
+- **Całkowicie darmowe**: Bez żadnych opłat
+- **Brak ograniczeń tłumaczenia**: Możesz tłumaczyć tyle razy, ile chcesz
+- **Szybkie**: Tłumaczenie jest szybkie, ponieważ przetwarzane jest lokalnie
+- **Prywatność**: Nie wymaga połączenia z internetem, dane nie są przesyłane na zewnątrz
+- **Stabilność**: Nie zależy od warunków sieciowych
+
+## pt-BR
+
+Módulo de tradução automática que funciona offline.
+
+### Vantagens
+- **Totalmente gratuito**: Sem custos
+- **Sem limite de tradução**: Traduza quantas vezes quiser
+- **Rápido**: Tradução rápida pois é processado localmente
+- **Privacidade**: Não requer conexão à internet, dados não são enviados externamente
+- **Estabilidade**: Não afetado pela rede
+
+## ru
+
+Модуль машинного перевода, работающий в автономном режиме.
+
+### Преимущества
+- **Полностью бесплатно**: Никаких платежей
+- **Без ограничений на перевод**: Вы можете переводить столько раз, сколько хотите
+- **Быстро**: Перевод быстрый, так как обрабатывается локально
+- **Конфиденциальность**: Не требуется подключение к интернету, данные не передаются наружу
+- **Стабильность**: Не зависит от состояния сети
+
+## th
+
+โมดูลการแปลด้วยเครื่องที่ทำงานแบบออฟไลน์
+
+### ข้อดี
+- **ฟรีทั้งหมด**: ไม่มีค่าใช้จ่ายใดๆ
+- **ไม่มีข้อจำกัดในการแปล**: คุณสามารถแปลได้มากเท่าที่ต้องการ
+- **เร็ว**: การแปลรวดเร็วเนื่องจากประมวลผลในเครื่อง
+- **ความเป็นส่วนตัว**: ไม่ต้องการการเชื่อมต่ออินเทอร์เน็ต ข้อมูลไม่ถูกส่งออกภายนอก
+- **เสถียรภาพ**: ไม่ได้รับผลกระทบจากสภาพเครือข่าย
+
+## tr
+
+Çevrimdışı çalışan bir makine çevirisi modülü.
+
+### Avantajlar
+- **Tamamen Ücretsiz**: Hiçbir ücret yok
+- **Çeviri Sınırı Yok**: İstediğiniz kadar çeviri yapabilirsiniz
+- **Hızlı**: Yerel olarak işlendiği için çeviri hızlıdır
+- **Gizlilik**: İnternet bağlantısı gerekmez, veriler dışarıya gönderilmez
+- **Kararlılık**: Ağ koşullarından etkilenmez
+
+## vi
+
+Mô-đun dịch máy hoạt động ngoại tuyến.
+
+### Ưu điểm
+- **Hoàn toàn miễn phí**: Hoàn toàn không tốn phí
+- **Không giới hạn dịch**: Có thể dịch không giới hạn lần
+- **Nhanh**: Dịch nhanh vì được xử lý cục bộ
+- **Quyền riêng tư**: Không cần kết nối internet, dữ liệu không được gửi ra bên ngoài
+- **Ổn định**: Không bị ảnh hưởng bởi mạng
+
+## zh-CN
+
+离线工作的机器翻译模块。
+
+### 优点
+- **完全免费**:完全不产生费用
+- **无翻译限制**:可以无限次翻译
+- **快速**:本地处理,翻译速度快
+- **隐私保护**:无需互联网连接,数据不会发送到外部
+- **稳定性**:不受网络影响
+
+## zh-TW
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+離線工作的機器翻譯模組。
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+### 優點
+- **完全免費**:完全不產生費用
+- **無翻譯限制**:可以無限次翻譯
+- **快速**:本地處理,翻譯速度快
+- **隱私保護**:無需網際網路連線,資料不會傳送到外部
+- **穩定性**:不受網路影響
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/README.md b/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/README.md
index d8bcbf79..60bda2cf 100644
--- a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/README.md
@@ -1,6 +1,8 @@
# WindowTranslator ColorThief Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) で、キャプチャ画像から翻訳テキストに適した前景色と背景色を推定するカラープラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)で、キャプチャ画像から翻訳テキストに適した前景色と背景色を推定するカラープラグインです。
## 機能
@@ -8,10 +10,216 @@
- 背景との明度差を考慮して読みやすい文字色を選択
- 回転したテキスト領域にも対応
-外部APIや追加設定は必要ありません。インストール後、対象設定のカラーモジュールで「近似カラー」を選択してください。
+外部APIや追加設定は必要ありません。対象設定のカラーモジュールで「近似カラー」を選択してください。
+
+## en
+
+A color plugin for [WindowTranslator](https://github.com/Freeesia/WindowTranslator) that estimates readable foreground and background colors for translated text from the captured image.
+
+## Features
+
+- Estimates the background color from representative colors around the OCR region
+- Selects a readable text color based on its brightness contrast with the background
+- Supports rotated text regions
+
+No external API or additional configuration is required. Select **Approximate Color** as the color module in the target settings.
+
+## ar
+
+مكوّن ألوان إضافي لـ[WindowTranslator](https://github.com/Freeesia/WindowTranslator) يقدّر ألوان المقدمة والخلفية المناسبة للنص المترجم من الصورة الملتقطة.
+
+### الميزات
+
+- يقدّر لون الخلفية من الألوان المحيطة بمنطقة OCR
+- يختار لون نص مقروء بناءً على فرق السطوع مع الخلفية
+- يدعم مناطق النص المدورة
+
+## cs
+
+Barevný plugin pro [WindowTranslator](https://github.com/Freeesia/WindowTranslator), který z pořízeného snímku odhaduje vhodné barvy popředí a pozadí přeloženého textu.
+
+### Funkce
+
+- Odhaduje barvu pozadí z barev v okolí oblasti OCR
+- Vybírá čitelnou barvu textu podle rozdílu jasu oproti pozadí
+- Podporuje otočené oblasti textu
+
+## de
+
+Ein Farb-Plugin für [WindowTranslator](https://github.com/Freeesia/WindowTranslator), das aus dem aufgenommenen Bild geeignete Vorder- und Hintergrundfarben für übersetzten Text ermittelt.
+
+### Funktionen
+
+- Ermittelt die Hintergrundfarbe aus den Farben um den OCR-Bereich
+- Wählt anhand des Helligkeitsunterschieds zum Hintergrund eine gut lesbare Textfarbe
+- Unterstützt gedrehte Textbereiche
+
+## es
+
+Un complemento de color para [WindowTranslator](https://github.com/Freeesia/WindowTranslator) que estima colores de primer plano y de fondo adecuados para el texto traducido a partir de la imagen capturada.
+
+### Funciones
+
+- Estima el color de fondo a partir de los colores alrededor del área de OCR
+- Selecciona un color de texto legible según la diferencia de brillo con el fondo
+- Admite áreas de texto giradas
+
+## fa
+
+افزونهٔ رنگ برای [WindowTranslator](https://github.com/Freeesia/WindowTranslator) که از تصویر گرفتهشده، رنگهای مناسب متن و پسزمینه را برای ترجمه برآورد میکند.
+
+### ویژگیها
+
+- رنگ پسزمینه را از رنگهای اطراف ناحیهٔ OCR برآورد میکند
+- بر اساس اختلاف روشنایی با پسزمینه، رنگ خوانایی برای متن انتخاب میکند
+- از نواحی متن چرخیده پشتیبانی میکند
+
+## fil
+
+Isang color plugin para sa [WindowTranslator](https://github.com/Freeesia/WindowTranslator) na tumataya ng angkop na kulay ng teksto at background mula sa nakuhang larawan.
+
+### Mga tampok
+
+- Tinataya ang kulay ng background mula sa mga kulay sa paligid ng OCR area
+- Pumipili ng nababasang kulay ng teksto batay sa diperensya ng liwanag sa background
+- Sinusuportahan ang mga iniikot na text area
+
+## fr
+
+Un plugin de couleur pour [WindowTranslator](https://github.com/Freeesia/WindowTranslator) qui estime, à partir de l’image capturée, les couleurs de premier plan et d’arrière-plan adaptées au texte traduit.
+
+### Fonctionnalités
+
+- Estime la couleur d’arrière-plan à partir des couleurs autour de la zone OCR
+- Sélectionne une couleur de texte lisible selon l’écart de luminosité avec l’arrière-plan
+- Prend en charge les zones de texte pivotées
+
+## hi
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator) के लिए एक रंग प्लगइन, जो कैप्चर की गई छवि से अनुवादित पाठ के लिए उपयुक्त अग्रभूमि और पृष्ठभूमि रंग का अनुमान लगाता है।
+
+### सुविधाएँ
+
+- OCR क्षेत्र के आसपास के रंगों से पृष्ठभूमि रंग का अनुमान लगाता है
+- पृष्ठभूमि के साथ चमक के अंतर के आधार पर पढ़ने योग्य पाठ रंग चुनता है
+- घुमाए गए पाठ क्षेत्रों का समर्थन करता है
+
+## hu
+
+Színbővítmény a [WindowTranslator](https://github.com/Freeesia/WindowTranslator) alkalmazáshoz, amely a rögzített képből megbecsüli a lefordított szöveg megfelelő előtér- és háttérszínét.
+
+### Funkciók
+
+- Az OCR-terület körüli színekből megbecsüli a háttérszínt
+- A háttérhez viszonyított fényerőkülönbség alapján jól olvasható szövegszínt választ
+- Támogatja az elforgatott szövegterületeket
+
+## id
+
+Plugin warna untuk [WindowTranslator](https://github.com/Freeesia/WindowTranslator) yang memperkirakan warna latar depan dan latar belakang yang sesuai untuk teks terjemahan dari gambar tangkapan.
+
+### Fitur
+
+- Memperkirakan warna latar belakang dari warna di sekitar area OCR
+- Memilih warna teks yang mudah dibaca berdasarkan perbedaan kecerahan dengan latar belakang
+- Mendukung area teks yang diputar
+
+## ko
+
+캡처 이미지에서 번역문에 적합한 전경색과 배경색을 추정하는 [WindowTranslator](https://github.com/Freeesia/WindowTranslator)용 색상 플러그인입니다.
+
+### 기능
+
+- OCR 영역 주변의 색상으로 배경색을 추정
+- 배경과의 밝기 차이를 기준으로 읽기 쉬운 글자색을 선택
+- 회전된 텍스트 영역 지원
+
+## ms
+
+Pemalam warna untuk [WindowTranslator](https://github.com/Freeesia/WindowTranslator) yang menganggarkan warna latar depan dan latar belakang yang sesuai untuk teks terjemahan daripada imej tangkapan.
+
+### Ciri
+
+- Menganggarkan warna latar belakang daripada warna di sekitar kawasan OCR
+- Memilih warna teks yang mudah dibaca berdasarkan perbezaan kecerahan dengan latar belakang
+- Menyokong kawasan teks yang diputar
+
+## pl
+
+Wtyczka kolorów dla [WindowTranslator](https://github.com/Freeesia/WindowTranslator), która na podstawie przechwyconego obrazu szacuje odpowiednie kolory tekstu i tła tłumaczenia.
+
+### Funkcje
+
+- Szacuje kolor tła na podstawie kolorów wokół obszaru OCR
+- Wybiera czytelny kolor tekstu według różnicy jasności względem tła
+- Obsługuje obrócone obszary tekstu
+
+## pt-BR
+
+Um plugin de cores para o [WindowTranslator](https://github.com/Freeesia/WindowTranslator) que estima cores adequadas de primeiro plano e de fundo para o texto traduzido a partir da imagem capturada.
+
+### Recursos
+
+- Estima a cor de fundo usando as cores ao redor da área de OCR
+- Seleciona uma cor de texto legível com base na diferença de brilho em relação ao fundo
+- Oferece suporte a áreas de texto giradas
+
+## ru
+
+Цветовой плагин для [WindowTranslator](https://github.com/Freeesia/WindowTranslator), который по захваченному изображению определяет подходящие цвета текста и фона для перевода.
+
+### Возможности
+
+- Определяет цвет фона по цветам вокруг области OCR
+- Выбирает читаемый цвет текста с учётом разницы яркости относительно фона
+- Поддерживает повёрнутые области текста
+
+## th
+
+ปลั๊กอินสีสำหรับ [WindowTranslator](https://github.com/Freeesia/WindowTranslator) ที่ประเมินสีข้อความและสีพื้นหลังที่เหมาะสมสำหรับข้อความแปลจากภาพที่จับไว้
+
+### คุณสมบัติ
+
+- ประเมินสีพื้นหลังจากสีรอบบริเวณ OCR
+- เลือกสีข้อความที่อ่านง่ายตามความแตกต่างของความสว่างกับพื้นหลัง
+- รองรับบริเวณข้อความที่หมุน
+
+## tr
+
+Yakalanan görüntüden çevrilmiş metin için uygun ön plan ve arka plan renklerini tahmin eden bir [WindowTranslator](https://github.com/Freeesia/WindowTranslator) renk eklentisidir.
+
+### Özellikler
+
+- OCR alanının çevresindeki renklerden arka plan rengini tahmin eder
+- Arka planla parlaklık farkına göre okunabilir bir metin rengi seçer
+- Döndürülmüş metin alanlarını destekler
+
+## vi
+
+Plugin màu cho [WindowTranslator](https://github.com/Freeesia/WindowTranslator), ước tính màu chữ và màu nền phù hợp cho văn bản dịch từ ảnh đã chụp.
+
+### Tính năng
+
+- Ước tính màu nền từ các màu xung quanh vùng OCR
+- Chọn màu chữ dễ đọc dựa trên độ chênh lệch sáng với nền
+- Hỗ trợ vùng văn bản bị xoay
+
+## zh-CN
+
+一个用于 [WindowTranslator](https://github.com/Freeesia/WindowTranslator) 的颜色插件,可从捕获的图像中估算适合译文的前景色和背景色。
+
+### 功能
+
+- 根据 OCR 区域周围的颜色估算背景色
+- 根据与背景的亮度差选择易读的文字颜色
+- 支持旋转的文本区域
+
+## zh-TW
-## インストール
+適用於 [WindowTranslator](https://github.com/Freeesia/WindowTranslator) 的色彩外掛程式,可從擷取的影像估算適合譯文的前景色與背景色。
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+### 功能
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+- 根據 OCR 區域周圍的色彩估算背景色
+- 根據與背景的亮度差選擇易讀的文字色彩
+- 支援旋轉的文字區域
diff --git a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/README.md b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/README.md
index 9dc70687..920e8bf0 100644
--- a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/README.md
@@ -1,10 +1,12 @@
# WindowTranslator DeepL Translator Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) でDeepL APIを利用する翻訳プラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)でDeepL APIを利用する翻訳プラグインです。
## 機能
-- DeepL APIによる翻訳
+- DeepL APIによる自然で高品質な翻訳
- WindowTranslatorから渡された文脈を翻訳リクエストへ反映
- CSV用語集による表記の統一
- 設定画面からAPI利用量を確認
@@ -15,8 +17,216 @@
- 用語集を利用する場合は、ヘッダーなしの`原文,訳文`形式のCSVファイルを指定します。
- 利用可能な言語、料金、上限はDeepL APIの契約内容に従います。
-## インストール
+## en
+
+A [WindowTranslator](https://github.com/Freeesia/WindowTranslator) translation plugin that uses the DeepL API.
+
+## Features
+
+- Natural, high-quality translation through the DeepL API
+- Includes context supplied by WindowTranslator in translation requests
+- Maintains consistent terminology with a CSV glossary
+- Shows API usage from the settings screen
+
+## Configuration
+
+- A DeepL API authentication key is required.
+- To use a glossary, specify a headerless CSV file in `source,target` format.
+- Available languages, pricing, and usage limits depend on your DeepL API plan.
+
+## ar
+
+وحدة تستخدم خدمة ترجمة DeepL، المعروفة بالترجمات عالية الجودة.
+
+### المزايا
+- **دقة عالية**: ترجمات طبيعية وعالية الجودة
+- **طبقة مجانية كبيرة**: حتى 500,000 حرف مجاناً شهرياً (API مجاني)
+- **سريع**: معالجة ترجمة سريعة
+- **دعم المسرد**: حافظ على اتساق الترجمة مع المسارد
+
+## cs
+
+Překladový modul využívající překladatelskou službu DeepL.
+
+### Výhody
+- **Vysoká přesnost překladu**: Vysoká kvalita překladů
+- **Velká bezplatná kvóta**: Velkorysý bezplatný limit
+- **Rychlost**: Rychlý překlad
+
+## de
+
+Ein Modul, das DeepLs Übersetzungsdienst verwendet, bekannt für hochwertige Übersetzungen.
+
+### Vorteile
+- **Hohe Genauigkeit**: Liefert natürliche, hochwertige Übersetzungen
+- **Großzügiges kostenloses Kontingent**: Bis zu 500.000 Zeichen pro Monat kostenlos (Free API)
+- **Schnell**: Schnelle Übersetzungsverarbeitung
+- **Glossar-Unterstützung**: Kann Übersetzungskonsistenz durch Glossare aufrechterhalten
+
+## es
+
+Un módulo que utiliza el servicio de traducción DeepL, conocido por traducciones de alta calidad.
+
+### Ventajas
+- **Alta precisión**: Traducciones naturales y de alta calidad
+- **Nivel gratuito sustancial**: Hasta 500,000 caracteres gratis por mes (API gratuita)
+- **Rápido**: Procesamiento de traducción rápido
+- **Soporte de glosario**: Mantenga la consistencia de traducción con glosarios
+
+## fa
+
+ماژولی که از سرویس ترجمه DeepL، معروف به ترجمههای با کیفیت بالا، استفاده میکند.
+
+### مزایا
+- **دقت بالا**: ترجمههای طبیعی و با کیفیت بالا
+- **طبقه رایگان بزرگ**: تا 500,000 کاراکتر در ماه رایگان (API رایگان)
+- **سریع**: پردازش ترجمه سریع
+- **پشتیبانی از واژهنامه**: حفظ ثبات ترجمه با واژهنامهها
+
+## fil
+
+Isang modyul na gumagamit ng serbisyo ng pagsasalin ng DeepL, kilala sa mataas na kalidad ng mga pagsasalin.
+
+### Mga Bentahe
+- **Mataas na Katumpakan**: Nagbibigay ng natural at mataas na kalidad ng mga pagsasalin
+- **Mapagbigay na Libreng Tier**: Hanggang 500,000 character bawat buwan nang libre (Free API)
+- **Mabilis**: Mabilis na pagproseso ng pagsasalin
+- **Suporta sa Glossary**: Maaaring mapanatili ang consistency ng pagsasalin gamit ang mga glossary
+
+## fr
+
+Un module utilisant le service de traduction DeepL, connu pour des traductions de haute qualité.
+
+### Avantages
+- **Haute précision**: Traductions naturelles et de haute qualité
+- **Offre gratuite substantielle**: Jusqu'à 500 000 caractères gratuits par mois (API gratuite)
+- **Rapide**: Traitement de traduction rapide
+- **Support de glossaire**: Maintenez la cohérence de traduction avec des glossaires
+
+## hi
+
+उच्च गुणवत्ता अनुवाद के लिए प्रसिद्ध DeepL अनुवाद सेवा का उपयोग करने वाला मॉड्यूल।
+
+### फायदे
+- **उच्च सटीकता**: प्राकृतिक, उच्च गुणवत्ता अनुवाद प्रदान करता है
+- **उदार निःशुल्क सीमा**: मासिक 5 लाख वर्ण तक निःशुल्क उपयोग (निःशुल्क API)
+- **तेज़**: तेज़ अनुवाद प्रसंस्करण
+- **शब्दावली समर्थन**: शब्दावली का उपयोग करके अनुवाद की स्थिरता बनाए रख सकते हैं
+
+## hu
+
+A DeepL fordítási szolgáltatását használó fordítási modul.
+
+### Előnyök
+- **Magas fordítási pontosság**: Magas minőségű fordítások
+- **Nagy ingyenes kvóta**: Nagyvonalú ingyenes limit
+- **Sebesség**: Gyors fordítás
+
+## id
+
+Modul menggunakan layanan terjemahan DeepL, terkenal dengan terjemahan berkualitas tinggi.
+
+### Keuntungan
+- **Akurasi Tinggi**: Menyediakan terjemahan alami berkualitas tinggi
+- **Tingkat Gratis yang Murah Hati**: Hingga 500.000 karakter per bulan secara gratis (API Gratis)
+- **Cepat**: Pemrosesan terjemahan cepat
+- **Dukungan Glosarium**: Dapat mempertahankan konsistensi terjemahan menggunakan glosarium
+
+## ko
+
+고품질 번역으로 알려진 DeepL의 번역 서비스를 사용하는 모듈입니다.
+
+### 장점
+- **높은 정확도**: 자연스럽고 고품질의 번역을 제공합니다
+- **넉넉한 무료 제공량**: 월 50만 자까지 무료로 사용할 수 있습니다 (무료 API)
+- **빠름**: 번역 처리가 빠릅니다
+- **용어집 지원**: 용어집을 이용하여 번역의 일관성을 유지할 수 있습니다
+
+## ms
+
+Modul menggunakan perkhidmatan terjemahan DeepL, terkenal dengan terjemahan berkualiti tinggi.
+
+### Kelebihan
+- **Ketepatan Tinggi**: Menyediakan terjemahan semula jadi berkualiti tinggi
+- **Peringkat Percuma yang Murah Hati**: Sehingga 500,000 aksara sebulan secara percuma (API Percuma)
+- **Pantas**: Pemprosesan terjemahan pantas
+- **Sokongan Glosari**: Boleh mengekalkan konsistensi terjemahan menggunakan glosari
+
+## pl
+
+Moduł tłumaczeniowy wykorzystujący usługę tłumaczeniową DeepL.
+
+### Zalety
+- **Wysoka dokładność tłumaczenia**: Wysoka jakość tłumaczeń
+- **Duży darmowy plan**: Hojny darmowy limit
+- **Szybkość**: Szybkie tłumaczenie
+
+## pt-BR
+
+Módulo que utiliza o serviço de tradução DeepL, conhecido por traduções de alta qualidade.
+
+### Vantagens
+- **Alta precisão**: Obtenha traduções naturais e de alta qualidade
+- **Ampla cota gratuita**: Use gratuitamente até 500.000 caracteres por mês (API gratuita)
+- **Rápido**: Processamento de tradução rápido
+- **Suporte a glossário**: Use glossários para manter consistência na tradução
+
+## ru
+
+Модуль, использующий службу перевода DeepL, известную высококачественными переводами.
+
+### Преимущества
+- **Высокая точность**: Обеспечивает естественные высококачественные переводы
+- **Щедрый бесплатный тариф**: До 500 000 символов в месяц бесплатно (Free API)
+- **Быстро**: Быстрая обработка перевода
+- **Поддержка глоссария**: Может поддерживать согласованность перевода с использованием глоссариев
+
+## th
+
+โมดูลที่ใช้บริการแปลของ DeepL ซึ่งเป็นที่รู้จักในด้านการแปลคุณภาพสูง
+
+### ข้อดี
+- **ความแม่นยำสูง**: ให้การแปลที่เป็นธรรมชาติและมีคุณภาพสูง
+- **แผนฟรีที่ใจกว้าง**: สูงสุด 500,000 ตัวอักษรต่อเดือนฟรี (Free API)
+- **เร็ว**: การประมวลผลการแปลรวดเร็ว
+- **รองรับอภิธานศัพท์**: สามารถรักษาความสอดคล้องของการแปลโดยใช้อภิธานศัพท์
+
+## tr
+
+Yüksek kaliteli çevirilerle tanınan DeepL'in çeviri hizmetini kullanan bir modül.
+
+### Avantajlar
+- **Yüksek Doğruluk**: Doğal, yüksek kaliteli çeviriler sağlar
+- **Cömert Ücretsiz Katman**: Ayda 500.000 karaktere kadar ücretsiz (Free API)
+- **Hızlı**: Hızlı çeviri işleme
+- **Sözlük Desteği**: Sözlükler kullanarak çeviri tutarlılığını koruyabilir
+
+## vi
+
+Mô-đun sử dụng dịch vụ dịch thuật DeepL được biết đến với chất lượng cao.
+
+### Ưu điểm
+- **Độ chính xác cao**: Cung cấp bản dịch tự nhiên và chất lượng cao
+- **Hạn mức miễn phí hào phóng**: Tối đa 500.000 ký tự mỗi tháng miễn phí (API miễn phí)
+- **Nhanh**: Xử lý dịch nhanh
+- **Hỗ trợ thuật ngữ**: Có thể duy trì tính nhất quán trong dịch thuật bằng cách sử dụng thuật ngữ
+
+## zh-CN
+
+使用以高质量翻译闻名的 DeepL 翻译服务的模块。
+
+### 优点
+- **高准确度**:提供自然、高质量的翻译
+- **充足的免费额度**:每月最多50万字符免费(免费 API)
+- **快速**:翻译处理速度快
+- **术语表支持**:可以使用术语表保持翻译一致性
+
+## zh-TW
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+使用以高品質翻譯聞名的 DeepL 翻譯服務的模組。
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+### 優點
+- **高準確度**:提供自然、高品質的翻譯
+- **充足的免費額度**:每月最多50萬字元免費(免費 API)
+- **快速**:翻譯處理速度快
+- **術語表支援**:可以使用術語表保持翻譯一致性
diff --git a/Plugins/WindowTranslator.Plugin.FoMPlugin/README.md b/Plugins/WindowTranslator.Plugin.FoMPlugin/README.md
index fd4d848b..da2cbdf4 100644
--- a/Plugins/WindowTranslator.Plugin.FoMPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.FoMPlugin/README.md
@@ -1,6 +1,8 @@
# WindowTranslator Fields of Mistria Filter Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) の翻訳をゲーム「Fields of Mistria」向けに補助するフィルタープラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)の翻訳をゲーム「Fields of Mistria」向けに補助するフィルタープラグインです。
## 機能
@@ -18,8 +20,202 @@
このプラグイン自体は翻訳サービスを提供しません。WindowTranslatorで別途、翻訳モジュールを選択してください。
-## インストール
+## en
+
+A filter plugin that adapts [WindowTranslator](https://github.com/Freeesia/WindowTranslator) translations for the game **Fields of Mistria**.
+
+## Features
+
+- Activates only while `FieldsOfMistria.exe` is running
+- Corrects OCR results using the game's `localization.json`
+- Adds character, scene, and dialogue information to the translation context
+- Registers character and item names as glossary entries with the selected translation module
+
+## Configuration
+
+- Enable or disable OCR correction
+- Use the official Japanese text
+- Set the player and farm names
+- Exclude text that is not present in the game data
+
+This plugin does not provide a translation service by itself. Select a translation module separately in WindowTranslator.
+
+## ar
+
+مخصص لـ Fields of Mistria
+
+- استخدام التصحيح بناءً على موارد اللعبة
+- استخدام الموارد اليابانية للعبة
+- استبعاد النص غير المحدد
+- اسم اللاعب / اسم المزرعة
+
+## cs
+
+Pouze pro Fields of Mistria
+
+- Použít opravy se zdroji obsaženými ve hře.
+- Použít japonské zdroje obsažené ve hře
+- Vyloučit nerozpoznaný text
+- Jméno hráče / Název farmy
+
+## de
+
+Exklusiv bei Fields of Mistria
+
+- Verwendung der im Spiel enthaltenen ressourcenbasierten Korrekturen.
+- Verwenden Sie die im Spiel enthaltenen japanischen Ressourcen.
+- Nicht identifizierbaren Text ausschließen
+- Spieler Name / Name des Betriebs
+
+## es
+
+Dedicado a Fields of Mistria
+
+- Usar corrección basada en recursos del juego
+- Usar recursos japoneses del juego
+- Excluir texto no identificable
+- Nombre del jugador / Nombre de la granja
+
+## fa
+
+مخصوص Fields of Mistria
+
+- استفاده از تصحیح بر اساس منابع بازی
+- استفاده از منابع ژاپنی بازی
+- حذف متن مشخصنشده
+- نام بازیکن / نام مزرعه
+
+## fil
+
+Eksklusibo para sa Fields of Mistria
+
+- Gamitin ang pagwawasto gamit ang mga mapagkukunang kasama sa laro
+- Gamitin ang mga Japanese na mapagkukunang kasama sa laro
+- Ibukod ang hindi matukoy na teksto
+- Pangalan ng Manlalaro / Pangalan ng Farm
+
+## fr
+
+Dédié à Fields of Mistria
+
+- Utiliser la correction basée sur les ressources du jeu
+- Utiliser les ressources japonaises du jeu
+- Exclure le texte non identifiable
+- Nom du joueur / Nom de la ferme
+
+## hi
+
+फील्ड्स ऑफ मिस्ट्रिया के लिए विशेष
+
+- गेम में शामिल संसाधनों के साथ क्षतिपूर्ति का उपयोग करें।
+- गेम में शामिल जापानी संसाधनों का उपयोग करें
+- अज्ञात टेक्स्ट बाहर करें
+- खिलाड़ी का नाम / फार्म का नाम
+
+## hu
+
+Fields of Mistria kizárólagos
+
+- Javítás használata a játékban lévő erőforrásokkal
+- A játékban lévő japán erőforrások használata
+- Ismeretlen szöveg kizárása
+- Játékos neve / Farm neve
+
+## id
+
+Eksklusif untuk Fields of Mistria
+
+- Gunakan kompensasi dengan sumber daya yang disertakan dalam game.
+- Gunakan sumber daya Jepang yang disertakan dalam game
+- Kecualikan teks yang tidak teridentifikasi
+- Player Name / Nama Pertanian
+
+## ko
+
+Fields of Mistria 전용
+
+- 게임에 포함된 리소스를 이용한 보정 활용하기
+- 게임에 포함된 일본어 리소스 이용하기
+- 특정할 수 없는 텍스트 제외
+- 플레이어 이름 / 농장명
+
+## ms
+
+Eksklusif untuk Fields of Mistria
+
+- Gunakan pampasan dengan sumber yang disertakan dalam permainan.
+- Gunakan sumber Jepun yang disertakan dalam permainan
+- Kecualikan teks tidak dikenal pasti
+- Player Name / Nama Ladang
+
+## pl
+
+Wyłącznie dla Fields of Mistria
+
+- Użyj rekompensaty z zasobami zawartymi w grze.
+- Użyj japońskich zasobów zawartych w grze
+- Wyklucz niezidentyfikowany tekst
+- Nazwa gracza / Nazwa farmy
+
+## pt-BR
+
+Exclusivo para Fields of Mistria
+
+- Use compensação com recursos incluídos no jogo.
+- Use recursos japoneses incluídos no jogo
+- Excluir texto não identificado
+- Player Name / Nome da Fazenda
+
+## ru
+
+Только для Fields of Mistria
+
+- Использовать исправление с помощью ресурсов, включенных в игру
+- Использовать японские ресурсы, включенные в игру
+- Исключить неопределенный текст
+- Имя игрока / Название фермы
+
+## th
+
+สำหรับ Fields of Mistria เท่านั้น
+
+- ใช้การแก้ไขด้วยทรัพยากรที่รวมอยู่ในเกม
+- ใช้ทรัพยากรภาษาญี่ปุ่นที่รวมอยู่ในเกม
+- ยกเว้นข้อความที่ระบุไม่ได้
+- ชื่อผู้เล่น / ชื่อฟาร์ม
+
+## tr
+
+Yalnızca Fields of Mistria için
+
+- Oyunda bulunan kaynakları kullanarak düzeltme kullan
+- Oyunda bulunan Japonca kaynakları kullan
+- Belirtilemeyen metni hariç tut
+- Oyuncu Adı / Çiftlik Adı
+
+## vi
+
+Fields of Mistria専用
+
+- ゲームに含まれているリソースを利用した補正を利用する
+- ゲームに含まれている日本語リソースを利用する
+- 特定できないテキストを除外
+- プレイヤー名 / 農場名
+
+## zh-CN
+
+迷雾之地》独有
+
+- 使用游戏中包含的基于资源的修正。
+- 使用游戏中包含的日语资源。
+- 排除无法识别的文本。
+- 球员姓名 / 农场名称
+
+## zh-TW
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+專屬於 Fields of Mistria
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+- 使用遊戲中包含的資源型修正。
+- 使用遊戲中包含的日文資源。
+- 排除無法辨識的文字。
+- 玩家名稱 / 農場名稱
diff --git a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/README.md b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/README.md
index 15474f86..77713bfd 100644
--- a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/README.md
@@ -1,6 +1,8 @@
# WindowTranslator GitHub Copilot Translator Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) でGitHub Copilotを利用する翻訳プラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)でGitHub Copilotを利用する翻訳プラグインです。
## 機能
@@ -17,8 +19,181 @@
利用可能なモデル、料金、上限はGitHub Copilotの契約内容に従います。
-## インストール
+## en
+
+A [WindowTranslator](https://github.com/Freeesia/WindowTranslator) translation plugin that uses GitHub Copilot.
+
+## Features
+
+- Context-aware translation with the selected Copilot model
+- Custom context for adding translation instructions
+- Consistent terminology through a CSV glossary
+- Uses conversation and game-specific context supplied by WindowTranslator
+
+## Requirements and configuration
+
+- A GitHub account with access to GitHub Copilot and completed authentication
+- The model name to use
+- Optionally, translation context and a headerless CSV glossary in `source,target` format
+
+Available models, pricing, and usage limits depend on your GitHub Copilot plan.
+
+## ar
+
+ترجمة GitHub Copilot
+
+- معرف النموذج المتاح على GitHub Copilot (مثال: gpt-4o، claude-sonnet-4.5)
+- معلومات السياق المستخدمة أثناء الترجمة
+- مسار المسرد
+
+## cs
+
+Překlad GitHub Copilot
+
+- ID modelu dostupného na GitHub Copilot (např. gpt-4o, claude-sonnet-4.5)
+- Kontextové informace použité při překladu
+- Cesta ke glosáři
+
+## de
+
+GitHub Copilot-Übersetzung
+
+- ID des auf GitHub Copilot verfügbaren Modells (z. B. gpt-4o, claude-sonnet-4.5)
+- Kontextinformationen für die Übersetzung
+- Glossarpfad
+
+## es
+
+Traducción GitHub Copilot
+
+- ID del modelo disponible en GitHub Copilot (ej: gpt-4o, claude-sonnet-4.5)
+- Información contextual utilizada durante la traducción
+- Ruta del glosario
+
+## fa
+
+ترجمه GitHub Copilot
+
+- شناسه مدل موجود در GitHub Copilot (مثال: gpt-4o، claude-sonnet-4.5)
+- اطلاعات زمینهای که هنگام ترجمه استفاده میشود
+- مسیر واژهنامه
+
+## fil
+
+Pagsasalin ng GitHub Copilot
+
+- ID ng modelong available sa GitHub Copilot (hal: gpt-4o, claude-sonnet-4.5)
+- Impormasyon ng konteksto na ginagamit sa panahon ng pagsasalin
+- Landas ng glossaryo
+
+## fr
+
+Traduction GitHub Copilot
+
+- ID du modèle disponible sur GitHub Copilot (ex: gpt-4o, claude-sonnet-4.5)
+- Informations contextuelles utilisées lors de la traduction
+- Chemin du glossaire
+
+## hi
+
+GitHub Copilot अनुवाद
+
+- GitHub Copilot पर उपलब्ध मॉडल की ID (उदा., gpt-4o, claude-sonnet-4.5)
+- अनुवाद के दौरान उपयोग की जाने वाली संदर्भ जानकारी
+- शब्दावली पथ
+
+## hu
+
+GitHub Copilot fordítás
+
+- A GitHub Copiloton elérhető modell azonosítója (pl. gpt-4o, claude-sonnet-4.5)
+- Fordítás során használandó kontextuális információk
+- Szószedet elérési útja
+
+## id
+
+Terjemahan GitHub Copilot
+
+- ID model yang tersedia di GitHub Copilot (contoh: gpt-4o, claude-sonnet-4.5)
+- Informasi konteks yang digunakan saat penerjemahan
+- Jalur glosarium
+
+## ko
+
+GitHub Copilot 번역
+
+- GitHub Copilot에서 사용 가능한 모델 ID (예: gpt-4o, claude-sonnet-4.5)
+- 번역 시 사용할 문맥 정보
+- 용어집 경로
+
+## ms
+
+Terjemahan GitHub Copilot
+
+- ID model yang tersedia pada GitHub Copilot (contoh: gpt-4o, claude-sonnet-4.5)
+- Maklumat konteks yang digunakan semasa terjemahan
+- Laluan glosari
+
+## pl
+
+Tłumaczenie GitHub Copilot
+
+- ID modelu dostępnego w GitHub Copilot (np: gpt-4o, claude-sonnet-4.5)
+- Informacje kontekstowe używane podczas tłumaczenia
+- Ścieżka do słownika
+
+## pt-BR
+
+Tradução GitHub Copilot
+
+- ID do modelo disponível no GitHub Copilot (ex: gpt-4o, claude-sonnet-4.5)
+- Informações de contexto usadas durante a tradução
+- Caminho do glossário
+
+## ru
+
+Перевод GitHub Copilot
+
+- ID модели, доступной в GitHub Copilot (например: gpt-4o, claude-sonnet-4.5)
+- Контекстная информация, используемая при переводе
+- Путь к глоссарию
+
+## th
+
+การแปล GitHub Copilot
+
+- ID ของโมเดลที่มีใน GitHub Copilot (เช่น: gpt-4o, claude-sonnet-4.5)
+- ข้อมูลบริบทที่ใช้ระหว่างการแปล
+- เส้นทางอภิธานศัพท์
+
+## tr
+
+GitHub Copilot Çevirisi
+
+- GitHub Copilot'ta mevcut modelin kimliği (örn: gpt-4o, claude-sonnet-4.5)
+- Çeviri sırasında kullanılan bağlam bilgisi
+- Sözlük yolu
+
+## vi
+
+Dịch thuật GitHub Copilot
+
+- ID mô hình có sẵn trên GitHub Copilot (ví dụ: gpt-4o, claude-sonnet-4.5)
+- Thông tin ngữ cảnh sử dụng khi dịch thuật
+- Đường dẫn thuật ngữ
+
+## zh-CN
+
+GitHub Copilot 翻译
+
+- GitHub Copilot 上可用的模型 ID(例如:gpt-4o、claude-sonnet-4.5)
+- 翻译时使用的上下文信息
+- 术语表路径
+
+## zh-TW
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+GitHub Copilot 翻譯
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+- GitHub Copilot 上可用的模型 ID(例如:gpt-4o、claude-sonnet-4.5)
+- 翻譯時使用的上下文資訊
+- 術語表路徑
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/README.md b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/README.md
index 3ed1b4c4..e229b111 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/README.md
@@ -1,10 +1,12 @@
# WindowTranslator Google AI Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) でGoogle AI(Gemini)を利用する多機能プラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)でGoogle AI(Gemini)を利用する多機能プラグインです。
## 機能
-- Geminiによる文脈を考慮した翻訳
+- Geminiによる文脈を考慮した高品質な翻訳
- 画像を直接送信するAI OCR
- OCRテキストまたは元画像を使った認識結果の補正
- カスタム翻訳コンテキスト、補正サンプル、CSV用語集
@@ -20,8 +22,201 @@ AI OCRとOCR補正は実験的な機能です。
画像やテキストは設定したGoogle AIサービスへ送信されます。利用可能なモデル、料金、上限はサービスの契約内容に従います。
-## インストール
+## en
+
+A multi-purpose [WindowTranslator](https://github.com/Freeesia/WindowTranslator) plugin that uses Google AI (Gemini).
+
+## Features
+
+- High-quality, context-aware translation with Gemini
+- AI OCR that sends the source image directly to the model
+- Correction of recognition results using OCR text or the original image
+- Custom translation context, correction examples, and CSV glossaries
+
+AI OCR and OCR correction are experimental features.
+
+## Requirements and configuration
+
+- A Google AI API key
+- The Gemini model to use; a preview model name can also be specified when necessary
+- When using OCR correction, select the correction method and waiting behavior
+- Glossaries use a headerless CSV file in `source,target` format
+
+Images and text are sent to the configured Google AI service. Available models, pricing, and usage limits depend on the service plan.
+
+## ar
+
+وحدة ترجمة تستخدم أحدث تقنيات الذكاء الاصطناعي من Google.
+
+### المزايا
+- **أعلى دقة**: ترجمات عالية الجودة جداً تفهم السياق
+- **المرونة**: تخصيص المطالبات لتعديل أسلوب الترجمة
+- **دعم المسرد**: حافظ على اتساق الترجمة مع المسارد
+
+## cs
+
+Překladový modul využívající Gemini AI od Googlu.
+
+### Výhody
+- **Vysoká přesnost překladu**: Vysoká kvalita překladů díky AI
+- **Kontextový překlad**: Chápe kontext a překládá přirozeně
+
+## de
+
+Ein Übersetzungsmodul, das Googles neueste KI-Technologie nutzt.
+
+### Vorteile
+- **Höchste Genauigkeit**: Fähig zu sehr hochwertiger Übersetzung mit Kontextverständnis
+- **Flexibilität**: Kann Prompts anpassen, um Übersetzungsstil zu justieren
+- **Glossar-Unterstützung**: Kann Übersetzungskonsistenz durch Glossare aufrechterhalten
+
+## es
+
+Un módulo de traducción que utiliza la última tecnología de IA de Google.
+
+### Ventajas
+- **Mayor precisión**: Traducciones de muy alta calidad que entienden el contexto
+- **Flexibilidad**: Personalice prompts para ajustar el estilo de traducción
+- **Soporte de glosario**: Mantenga la consistencia de traducción con glosarios
+
+## fa
+
+ماژول ترجمهای که از جدیدترین فناوری هوش مصنوعی Google استفاده میکند.
+
+### مزایا
+- **بالاترین دقت**: ترجمههای با کیفیت بسیار بالا که زمینه را درک میکنند
+- **انعطافپذیری**: سفارشیسازی دستورالعمل برای تنظیم سبک ترجمه
+- **پشتیبانی از واژهنامه**: حفظ ثبات ترجمه با واژهنامهها
+
+## fil
+
+Isang modyul ng pagsasalin na gumagamit ng pinakabagong teknolohiya ng AI ng Google.
+
+### Mga Bentahe
+- **Pinakamataas na Katumpakan**: Kayang mag-translate ng napakataas na kalidad na may pag-unawa sa konteksto
+- **Kakayahang umangkop**: Maaaring i-customize ang mga prompt upang ayusin ang estilo ng pagsasalin
+- **Suporta sa Glossary**: Maaaring mapanatili ang consistency ng pagsasalin gamit ang mga glossary
+
+## fr
+
+Un module de traduction utilisant la dernière technologie IA de Google.
+
+### Avantages
+- **Plus haute précision**: Traductions de très haute qualité qui comprennent le contexte
+- **Flexibilité**: Personnalisez les prompts pour ajuster le style de traduction
+- **Support de glossaire**: Maintenez la cohérence de traduction avec des glossaires
+
+## hi
+
+Google की नवीनतम AI तकनीक का उपयोग करने वाला अनुवाद मॉड्यूल।
+
+### फायदे
+- **सर्वोच्च सटीकता**: संदर्भ को समझकर बहुत उच्च गुणवत्ता अनुवाद की क्षमता
+- **लचीलापन**: अनुवाद शैली को समायोजित करने के लिए प्रॉम्प्ट को कस्टमाइज़ कर सकते हैं
+- **शब्दावली समर्थन**: शब्दावली का उपयोग करके अनुवाद की स्थिरता बनाए रख सकते हैं
+
+## hu
+
+A Google Gemini AI-t használó fordítási modul.
+
+### Előnyök
+- **Magas fordítási pontosság**: Magas minőségű fordítások AI segítségével
+- **Kontextuális fordítás**: Érti a kontextust és természetesen fordít
+
+## id
+
+Modul terjemahan yang memanfaatkan teknologi AI terbaru Google.
+
+### Keuntungan
+- **Akurasi Tertinggi**: Mampu melakukan terjemahan berkualitas sangat tinggi dengan pemahaman kontekstual
+- **Fleksibilitas**: Dapat menyesuaikan prompt untuk menyesuaikan gaya terjemahan
+- **Dukungan Glosarium**: Dapat mempertahankan konsistensi terjemahan menggunakan glosarium
+
+## ko
+
+Google의 최신 AI 기술을 활용한 번역 모듈입니다.
+
+### 장점
+- **최고 정확도**: 문맥을 이해한 매우 고품질의 번역이 가능합니다
+- **유연성**: 프롬프트를 커스터마이즈하여 번역 스타일을 조정할 수 있습니다
+- **용어집 지원**: 용어집을 이용하여 번역의 일관성을 유지할 수 있습니다
+
+## ms
+
+Modul terjemahan yang memanfaatkan teknologi AI terkini Google.
+
+### Kelebihan
+- **Ketepatan Tertinggi**: Mampu melakukan terjemahan berkualiti sangat tinggi dengan pemahaman kontekstual
+- **Fleksibiliti**: Boleh menyesuaikan prompt untuk melaraskan gaya terjemahan
+- **Sokongan Glosari**: Boleh mengekalkan konsistensi terjemahan menggunakan glosari
+
+## pl
+
+Moduł tłumaczeniowy wykorzystujący Gemini AI od Google.
+
+### Zalety
+- **Wysoka dokładność tłumaczenia**: Wysoka jakość tłumaczeń dzięki AI
+- **Kontekstowe tłumaczenie**: Rozumie kontekst i tłumaczy naturalnie
+
+## pt-BR
+
+Módulo de tradução que utiliza a mais recente tecnologia de IA do Google.
+
+### Vantagens
+- **Máxima precisão**: Permite traduções de altíssima qualidade com compreensão de contexto
+- **Flexibilidade**: Customize prompts para ajustar o estilo de tradução
+- **Suporte a glossário**: Use glossários para manter consistência na tradução
+
+## ru
+
+Модуль перевода, использующий новейшие технологии искусственного интеллекта Google.
+
+### Преимущества
+- **Наивысшая точность**: Способен на очень высококачественный перевод с пониманием контекста
+- **Гибкость**: Можно настраивать подсказки для настройки стиля перевода
+- **Поддержка глоссария**: Может поддерживать согласованность перевода с использованием глоссариев
+
+## th
+
+โมดูลการแปลที่ใช้เทคโนโลยี AI ล่าสุดของ Google
+
+### ข้อดี
+- **ความแม่นยำสูงสุด**: สามารถแปลคุณภาพสูงมากด้วยความเข้าใจบริบท
+- **ความยืดหยุ่น**: สามารถปรับแต่ง prompt เพื่อปรับสไตล์การแปล
+- **รองรับอภิธานศัพท์**: สามารถรักษาความสอดคล้องของการแปลโดยใช้อภิธานศัพท์
+
+## tr
+
+Google'ın en son yapay zeka teknolojisinden yararlanan bir çeviri modülü.
+
+### Avantajlar
+- **En Yüksek Doğruluk**: Bağlamsal anlayışla çok yüksek kaliteli çeviri yapabilir
+- **Esneklik**: Çeviri stilini ayarlamak için istemler özelleştirebilir
+- **Sözlük Desteği**: Sözlükler kullanarak çeviri tutarlılığını koruyabilir
+
+## vi
+
+Mô-đun dịch thuật tận dụng công nghệ AI mới nhất của Google.
+
+### Ưu điểm
+- **Độ chính xác cao nhất**: Có khả năng dịch chất lượng rất cao với hiểu biết ngữ cảnh
+- **Linh hoạt**: Có thể tùy chỉnh prompt để điều chỉnh phong cách dịch
+- **Hỗ trợ thuật ngữ**: Có thể duy trì tính nhất quán trong dịch thuật bằng cách sử dụng thuật ngữ
+
+## zh-CN
+
+利用 Google 最新 AI 技术的翻译模块。
+
+### 优点
+- **最高准确度**:能够理解上下文进行极高质量的翻译
+- **灵活性**:可以自定义提示来调整翻译风格
+- **术语表支持**:可以使用术语表保持翻译一致性
+
+## zh-TW
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+利用 Google 最新 AI 技術的翻譯模組。
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+### 優點
+- **最高準確度**:能夠理解上下文進行極高品質的翻譯
+- **靈活性**:可以自訂提示來調整翻譯風格
+- **術語表支援**:可以使用術語表保持翻譯一致性
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/README.md b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/README.md
index 517b75c8..ff005259 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/README.md
@@ -1,6 +1,8 @@
# WindowTranslator Google Apps Script Translator Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) からGoogle Apps Scriptを呼び出して翻訳するプラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)からGoogle Apps Scriptを呼び出して翻訳するプラグインです。
## 機能
@@ -14,8 +16,198 @@
- 独自スクリプトを利用する場合は、Google Apps Script APIから実行可能なデプロイIDを設定します。
- インターネット接続が必要です。利用量や実行上限はGoogle側の制限に従います。
-## インストール
+## en
+
+A translation plugin that invokes Google Apps Script from [WindowTranslator](https://github.com/Freeesia/WindowTranslator).
+
+## Features
+
+- Translates multiple text entries through Google Apps Script
+- Supports the built-in published script or a custom Apps Script deployment
+- Automatically sends WindowTranslator's source and target languages to the script
+
+## Requirements and configuration
+
+- Using the built-in script requires authentication with a Google account.
+- To use a custom script, specify a deployment ID that can be executed through the Google Apps Script API.
+- An internet connection is required. Usage and execution limits are subject to Google's service limits.
+
+## ar
+
+وحدة ترجمة تستخدم خدمة ترجمة Google.
+
+### المزايا
+- **مجاني تماماً**: يمكن استخدامه بدون مفتاح API
+- **دعم متعدد اللغات**: يدعم العديد من أزواج اللغات
+- **سهل**: لا حاجة لإعداد خاص
+
+## cs
+
+Překladový modul využívající překladatelskou službu Google.
+
+### Výhody
+- **Zcela zdarma**: Lze používat bez klíče API
+- **Vícejazyčná podpora**: Podporuje mnoho jazykových párů
+- **Snadné**: Nevyžaduje speciální nastavení
+
+## de
+
+Ein Übersetzungsmodul, das Googles Übersetzungsdienst verwendet.
+
+### Vorteile
+- **Völlig kostenlos**: Kann ohne API-Schlüssel verwendet werden
+- **Mehrsprachige Unterstützung**: Unterstützt viele Sprachpaare
+- **Einfach**: Keine spezielle Konfiguration erforderlich
+
+## es
+
+Un módulo de traducción que utiliza el servicio de traducción de Google.
+
+### Ventajas
+- **Completamente gratis**: Se puede usar sin clave API
+- **Soporte multilingüe**: Admite muchos pares de idiomas
+- **Fácil**: No se requiere configuración especial
+
+## fa
+
+ماژول ترجمهای که از سرویس ترجمه Google استفاده میکند.
+
+### مزایا
+- **کاملاً رایگان**: میتوان بدون کلید API استفاده کرد
+- **پشتیبانی از چند زبان**: از جفتهای زبانی زیادی پشتیبانی میکند
+- **آسان**: نیاز به تنظیم خاصی ندارد
+
+## fil
+
+Isang modyul ng pagsasalin na gumagamit ng serbisyo ng pagsasalin ng Google.
+
+### Mga Bentahe
+- **Lubos na Libre**: Maaaring gamitin nang walang API key
+- **Suporta sa Maraming Wika**: Sumusuporta sa maraming language pair
+- **Madali**: Walang kinakailangang special na configuration
+
+## fr
+
+Un module de traduction utilisant le service de traduction de Google.
+
+### Avantages
+- **Complètement gratuit**: Peut être utilisé sans clé API
+- **Support multilingue**: Prend en charge de nombreuses paires de langues
+- **Facile**: Aucune configuration spéciale requise
+
+## hi
+
+Google की अनुवाद सेवा का उपयोग करने वाला अनुवाद मॉड्यूल।
+
+### फायदे
+- **पूर्ण रूप से निःशुल्क**: API कुंजी के बिना उपयोग कर सकते हैं
+- **बहुभाषी सहायता**: कई भाषा जोड़ों का समर्थन करता है
+- **सरल**: विशेष सेटअप की आवश्यकता नहीं
+
+## hu
+
+A Google fordítási szolgáltatását használó fordítási modul.
+
+### Előnyök
+- **Teljesen ingyenes**: API kulcs nélkül is használható
+- **Többnyelvű támogatás**: Sok nyelvpárt támogat
+- **Egyszerű**: Nem igényel speciális beállítást
+
+## id
+
+Modul terjemahan menggunakan layanan terjemahan Google.
+
+### Keuntungan
+- **Sepenuhnya Gratis**: Dapat digunakan tanpa kunci API
+- **Dukungan Multibahasa**: Mendukung banyak pasangan bahasa
+- **Mudah**: Tidak ada konfigurasi khusus yang diperlukan
+
+## ko
+
+Google의 번역 서비스를 사용하는 번역 모듈입니다.
+
+### 장점
+- **완전 무료**: API 키 없이 사용할 수 있습니다
+- **다국어 지원**: 많은 언어 쌍을 지원합니다
+- **간단함**: 특별한 구성이 필요 없습니다
+
+## ms
+
+Modul terjemahan menggunakan perkhidmatan terjemahan Google.
+
+### Kelebihan
+- **Percuma Sepenuhnya**: Boleh digunakan tanpa kunci API
+- **Sokongan Berbilang Bahasa**: Menyokong banyak pasangan bahasa
+- **Mudah**: Tiada konfigurasi khas diperlukan
+
+## pl
+
+Moduł tłumaczeniowy wykorzystujący usługę tłumaczeniową Google.
+
+### Zalety
+- **Całkowicie darmowe**: Można używać bez klucza API
+- **Obsługa wielojęzyczna**: Obsługuje wiele par językowych
+- **Łatwość**: Nie wymaga specjalnej konfiguracji
+
+## pt-BR
+
+Módulo de tradução que utiliza o serviço de tradução do Google.
+
+### Vantagens
+- **Totalmente gratuito**: Pode ser usado sem chave de API
+- **Suporte multilíngue**: Suporta muitos pares de idiomas
+- **Fácil**: Não requer configuração especial
+
+## ru
+
+Модуль перевода, использующий службу перевода Google.
+
+### Преимущества
+- **Полностью бесплатно**: Можно использовать без API-ключа
+- **Многоязычная поддержка**: Поддерживает множество языковых пар
+- **Легко**: Не требуется специальная настройка
+
+## th
+
+โมดูลการแปลที่ใช้บริการแปลของ Google
+
+### ข้อดี
+- **ฟรีทั้งหมด**: สามารถใช้ได้โดยไม่ต้องมี API key
+- **รองรับหลายภาษา**: รองรับคู่ภาษามากมาย
+- **ง่าย**: ไม่ต้องการการตั้งค่าพิเศษ
+
+## tr
+
+Google'ın çeviri hizmetini kullanan bir çeviri modülü.
+
+### Avantajlar
+- **Tamamen Ücretsiz**: API anahtarı olmadan kullanılabilir
+- **Çok Dilli Destek**: Birçok dil çiftini destekler
+- **Kolay**: Özel yapılandırma gerektirmez
+
+## vi
+
+Mô-đun dịch thuật sử dụng dịch vụ dịch của Google.
+
+### Ưu điểm
+- **Hoàn toàn miễn phí**: Có thể sử dụng mà không cần API key
+- **Hỗ trợ đa ngôn ngữ**: Hỗ trợ nhiều cặp ngôn ngữ
+- **Đơn giản**: Không cần cấu hình đặc biệt
+
+## zh-CN
+
+使用 Google 翻译服务的翻译模块。
+
+### 优点
+- **完全免费**:无需 API 密钥即可使用
+- **多语言支持**:支持多种语言对
+- **简单**:无需特殊配置
+
+## zh-TW
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+使用 Google 翻譯服務的翻譯模組。
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+### 優點
+- **完全免費**:無需 API 金鑰即可使用
+- **多語言支援**:支援多種語言對
+- **簡單**:無需特殊設定
diff --git a/Plugins/WindowTranslator.Plugin.LLMPlugin/README.md b/Plugins/WindowTranslator.Plugin.LLMPlugin/README.md
index 3502e059..24a94a0e 100644
--- a/Plugins/WindowTranslator.Plugin.LLMPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.LLMPlugin/README.md
@@ -1,10 +1,12 @@
# WindowTranslator LLM Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) でOpenAI互換APIを利用する多機能プラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)でOpenAI互換APIを利用する多機能プラグインです。
## 機能
-- 大規模言語モデルによる文脈を考慮した翻訳
+- 大規模言語モデルによる文脈を考慮した高品質な翻訳
- 画像対応モデルを利用したAI OCR
- OCRテキストまたは元画像を使った認識結果の補正
- OpenAI APIと互換エンドポイントの両方に対応
@@ -21,8 +23,219 @@ AI OCRとOCR補正は実験的な機能です。
画像やテキストは設定したAPIへ送信されます。料金、上限、データの扱いは利用するサービスの契約内容に従います。
-## インストール
+## en
+
+A multi-purpose [WindowTranslator](https://github.com/Freeesia/WindowTranslator) plugin for OpenAI-compatible APIs.
+
+## Features
+
+- High-quality, context-aware translation with large language models
+- AI OCR using vision-capable models
+- Correction of recognition results using OCR text or the original image
+- Supports both the OpenAI API and compatible endpoints
+- Custom translation context, correction examples, and CSV glossaries
+
+AI OCR and OCR correction are experimental features.
+
+## Requirements and configuration
+
+- The model name to use
+- An API key when required by the service
+- An OpenAI-compatible endpoint when using another provider
+- A headerless CSV glossary in `source,target` format when terminology control is needed
+
+Images and text are sent to the configured API. Pricing, usage limits, and data handling depend on the selected service.
+
+## ar
+
+وحدة ترجمة تستخدم ChatGPT API أو LLM محلي.
+
+### المزايا
+- **أعلى دقة**: ترجمات عالية الجودة بواسطة نماذج اللغة الكبيرة
+- **المرونة**: تخصيص المطالبات لتعديل أسلوب الترجمة
+- **دعم المسرد**: حافظ على اتساق الترجمة مع المسارد
+- **دعم LLM محلي**: إمكانية استخدام خادم LLM الخاص بك
+
+## cs
+
+Překladový modul využívající ChatGPT od OpenAI.
+
+### Výhody
+- **Vysoká přesnost překladu**: Vysoká kvalita překladů díky AI
+- **Přirozený překlad**: Překlady znějí přirozeně
+
+## de
+
+Ein Übersetzungsmodul, das ChatGPT API oder lokales LLM verwendet.
+
+### Vorteile
+- **Höchste Genauigkeit**: Hochwertige Übersetzung durch große Sprachmodelle
+- **Flexibilität**: Kann Prompts anpassen, um Übersetzungsstil zu justieren
+- **Glossar-Unterstützung**: Kann Übersetzungskonsistenz durch Glossare aufrechterhalten
+- **Lokale LLM-Unterstützung**: Kann auch eigenen LLM-Server verwenden
+
+## es
+
+Un módulo de traducción que utiliza la API de ChatGPT o un LLM local.
+
+### Ventajas
+- **Mayor precisión**: Traducciones de alta calidad por grandes modelos de lenguaje
+- **Flexibilidad**: Personalice prompts para ajustar el estilo de traducción
+- **Soporte de glosario**: Mantenga la consistencia de traducción con glosarios
+- **Soporte LLM local**: Posibilidad de usar su propio servidor LLM
+
+## fa
+
+ماژول ترجمهای که از ChatGPT API یا LLM محلی استفاده میکند.
+
+### مزایا
+- **بالاترین دقت**: ترجمههای با کیفیت بالا توسط مدلهای زبانی بزرگ
+- **انعطافپذیری**: سفارشیسازی دستورالعمل برای تنظیم سبک ترجمه
+- **پشتیبانی از واژهنامه**: حفظ ثبات ترجمه با واژهنامهها
+- **پشتیبانی از LLM محلی**: امکان استفاده از سرور LLM خودتان
+
+## fil
+
+Isang modyul ng pagsasalin na gumagamit ng ChatGPT API o local LLM.
+
+### Mga Bentahe
+- **Pinakamataas na Katumpakan**: Mataas na kalidad ng pagsasalin ng malalaking modelo ng wika
+- **Kakayahang umangkop**: Maaaring i-customize ang mga prompt upang ayusin ang estilo ng pagsasalin
+- **Suporta sa Glossary**: Maaaring mapanatili ang consistency ng pagsasalin gamit ang mga glossary
+- **Suporta sa Local LLM**: Maaari ring gamitin ang sarili mong LLM server
+
+## fr
+
+Un module de traduction utilisant l'API ChatGPT ou un LLM local.
+
+### Avantages
+- **Plus haute précision**: Traductions de haute qualité par grands modèles de langage
+- **Flexibilité**: Personnalisez les prompts pour ajuster le style de traduction
+- **Support de glossaire**: Maintenez la cohérence de traduction avec des glossaires
+- **Support LLM local**: Possibilité d'utiliser votre propre serveur LLM
+
+## hi
+
+ChatGPT API या स्थानीय LLM का उपयोग करने वाला अनुवाद मॉड्यूल।
+
+### फायदे
+- **सर्वोच्च सटीकता**: बड़े भाषा मॉडल द्वारा उच्च गुणवत्ता अनुवाद
+- **लचीलापन**: अनुवाद शैली को समायोजित करने के लिए प्रॉम्प्ट को कस्टमाइज़ कर सकते हैं
+- **शब्दावली समर्थन**: शब्दावली का उपयोग करके अनुवाद की स्थिरता बनाए रख सकते हैं
+- **स्थानीय LLM समर्थन**: अपना LLM सर्वर भी उपयोग कर सकते हैं
+
+## hu
+
+Az OpenAI ChatGPT-jét használó fordítási modul.
+
+### Előnyök
+- **Magas fordítási pontosság**: Magas minőségű fordítások AI segítségével
+- **Természetes fordítás**: A fordítások természetesen hangzanak
+
+## id
+
+Modul terjemahan menggunakan API ChatGPT atau LLM lokal.
+
+### Keuntungan
+- **Akurasi Tertinggi**: Terjemahan berkualitas tinggi oleh model bahasa besar
+- **Fleksibilitas**: Dapat menyesuaikan prompt untuk menyesuaikan gaya terjemahan
+- **Dukungan Glosarium**: Dapat mempertahankan konsistensi terjemahan menggunakan glosarium
+- **Dukungan LLM Lokal**: Juga dapat menggunakan server LLM Anda sendiri
+
+## ko
+
+ChatGPT API 또는 로컬 LLM을 사용하는 번역 모듈입니다.
+
+### 장점
+- **최고 정확도**: 대규모 언어 모델에 의한 고품질 번역
+- **유연성**: 프롬프트를 커스터마이즈하여 번역 스타일을 조정할 수 있습니다
+- **용어집 지원**: 용어집을 이용하여 번역의 일관성을 유지할 수 있습니다
+- **로컬 LLM 지원**: 자체 LLM 서버도 사용 가능
+
+## ms
+
+Modul terjemahan menggunakan API ChatGPT atau LLM tempatan.
+
+### Kelebihan
+- **Ketepatan Tertinggi**: Terjemahan berkualiti tinggi oleh model bahasa besar
+- **Fleksibiliti**: Boleh menyesuaikan prompt untuk melaraskan gaya terjemahan
+- **Sokongan Glosari**: Boleh mengekalkan konsistensi terjemahan menggunakan glosari
+- **Sokongan LLM Tempatan**: Juga boleh menggunakan pelayan LLM anda sendiri
+
+## pl
+
+Moduł tłumaczeniowy wykorzystujący ChatGPT od OpenAI.
+
+### Zalety
+- **Wysoka dokładność tłumaczenia**: Wysoka jakość tłumaczeń dzięki AI
+- **Naturalne tłumaczenie**: Tłumaczenia brzmią naturalnie
+
+## pt-BR
+
+Módulo de tradução que utiliza ChatGPT API ou LLM local.
+
+### Vantagens
+- **Máxima precisão**: Traduções de alta qualidade por modelos de linguagem em grande escala
+- **Flexibilidade**: Customize prompts para ajustar o estilo de tradução
+- **Suporte a glossário**: Use glossários para manter consistência na tradução
+- **Suporte a LLM local**: Também pode usar seu próprio servidor LLM
+
+## ru
+
+Модуль перевода, использующий ChatGPT API или локальный LLM.
+
+### Преимущества
+- **Наивысшая точность**: Высококачественный перевод большими языковыми моделями
+- **Гибкость**: Можно настраивать подсказки для настройки стиля перевода
+- **Поддержка глоссария**: Может поддерживать согласованность перевода с использованием глоссариев
+- **Поддержка локального LLM**: Также можно использовать собственный сервер LLM
+
+## th
+
+โมดูลการแปลที่ใช้ ChatGPT API หรือ LLM ในเครื่อง
+
+### ข้อดี
+- **ความแม่นยำสูงสุด**: การแปลคุณภาพสูงโดยโมเดลภาษาขนาดใหญ่
+- **ความยืดหยุ่น**: สามารถปรับแต่ง prompt เพื่อปรับสไตล์การแปล
+- **รองรับอภิธานศัพท์**: สามารถรักษาความสอดคล้องของการแปลโดยใช้อภิธานศัพท์
+- **รองรับ LLM ในเครื่อง**: สามารถใช้เซิร์ฟเวอร์ LLM ของคุณเองได้
+
+## tr
+
+ChatGPT API veya yerel LLM kullanan bir çeviri modülü.
+
+### Avantajlar
+- **En Yüksek Doğruluk**: Büyük dil modelleri tarafından yüksek kaliteli çeviri
+- **Esneklik**: Çeviri stilini ayarlamak için istemler özelleştirebilir
+- **Sözlük Desteği**: Sözlükler kullanarak çeviri tutarlılığını koruyabilir
+- **Yerel LLM Desteği**: Kendi LLM sunucunuzu da kullanabilirsiniz
+
+## vi
+
+Mô-đun dịch thuật sử dụng ChatGPT API hoặc LLM cục bộ.
+
+### Ưu điểm
+- **Độ chính xác cao nhất**: Bản dịch chất lượng cao bằng các mô hình ngôn ngữ lớn
+- **Linh hoạt**: Có thể tùy chỉnh prompt để điều chỉnh phong cách dịch
+- **Hỗ trợ thuật ngữ**: Có thể duy trì tính nhất quán trong dịch thuật bằng cách sử dụng thuật ngữ
+- **Hỗ trợ LLM cục bộ**: Cũng có thể sử dụng máy chủ LLM của riêng bạn
+
+## zh-CN
+
+使用 ChatGPT API 或本地 LLM 的翻译模块。
+
+### 优点
+- **最高准确度**:通过大型语言模型实现高质量翻译
+- **灵活性**:可以自定义提示来调整翻译风格
+- **术语表支持**:可以使用术语表保持翻译一致性
+- **本地 LLM 支持**:也可以使用自己的 LLM 服务器
+
+## zh-TW
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+使用 ChatGPT API 或本地 LLM 的翻譯模組。
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+### 優點
+- **最高準確度**:透過大型語言模型實現高品質翻譯
+- **靈活性**:可以自訂提示來調整翻譯風格
+- **術語表支援**:可以使用術語表保持翻譯一致性
+- **本地 LLM 支援**:也可以使用自己的 LLM 伺服器
diff --git a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/README.md b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/README.md
index 7975b2b0..23af9fc9 100644
--- a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/README.md
@@ -1,10 +1,12 @@
# WindowTranslator OneOCR Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) で、WindowsのSnipping Toolに含まれるOneOCRエンジンを利用するOCRプラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)で、WindowsのSnipping Toolに含まれるOneOCRエンジンを利用するOCRプラグインです。
## 機能
-- WindowsのローカルOCRエンジンによる高速な文字認識
+- WindowsのローカルOCRエンジンによる高速で高精度な文字認識
- OCR領域の結合、傾き、拡大率、明るさ、コントラストを考慮した後処理
- 認識結果から翻訳先言語のテキストを除外
@@ -15,12 +17,187 @@
対応するSnipping Toolが見つからない場合は、WindowTranslatorからMicrosoft Storeを開いて更新できます。OneOCR本体とモデルは、このNuGetパッケージには含まれません。
-## インストール
+## 参考
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+- [SnippingToolOcrSharp](https://github.com/ksasao/SnippingToolOcrSharp)
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+## en
-## 参考
+An OCR plugin for [WindowTranslator](https://github.com/Freeesia/WindowTranslator) that uses the OneOCR engine included with Windows Snipping Tool.
+
+## Features
+
+- Fast, high-accuracy text recognition with a local Windows OCR engine
+- Post-processing that accounts for merged OCR regions, rotation, scale, brightness, and contrast
+- Excludes text written in the translation target language from recognition results
+
+## Requirements
+
+- A supported version of Windows Snipping Tool that includes OneOCR
+- Permission during initial setup to copy the required OneOCR components from Snipping Tool to WindowTranslator's shared data directory
+
+If a supported Snipping Tool cannot be found, WindowTranslator can open Microsoft Store so it can be updated. The OneOCR engine and models are not included in this NuGet package.
+
+## Reference
- [SnippingToolOcrSharp](https://github.com/ksasao/SnippingToolOcrSharp)
+
+## ar
+
+وحدة OCR محلية مقدمة من Microsoft.
+
+### المزايا
+- **دقة التعرف**: أعلى دقة في التعرف
+- **سريع**: سرعة معالجة عالية جداً
+
+## cs
+
+Místní modul OCR dodaný společností Microsoft.
+
+### Výhody
+- **Přesnost rozpoznávání**: Může se pochlubit nejvyšší přesností rozpoznávání
+- **Rychlost**: Velmi vysoká rychlost zpracování
+
+## de
+
+Ein lokales OCR-Modul von Microsoft.
+
+### Vorteile
+- **Erkennungsgenauigkeit**: Verfügt über die höchste Erkennungsgenauigkeit
+- **Schnell**: Sehr hohe Verarbeitungsgeschwindigkeit
+
+## es
+
+Un módulo OCR local proporcionado por Microsoft.
+
+### Ventajas
+- **Precisión de reconocimiento**: La más alta precisión de reconocimiento
+- **Rápido**: Velocidad de procesamiento muy rápida
+
+## fa
+
+ماژول OCR محلی ارائهشده توسط Microsoft.
+
+### مزایا
+- **دقت تشخیص**: بالاترین دقت تشخیص
+- **سریع**: سرعت پردازش بسیار بالا
+
+## fil
+
+Isang lokal na modyul ng OCR na ibinigay ng Microsoft.
+
+### Mga Bentahe
+- **Katumpakan ng Pagkilala**: Nag-aanyaya ng pinakamataas na katumpakan ng pagkilala
+- **Mabilis**: Napakabilis ng bilis ng pagproseso
+
+## fr
+
+Un module OCR local fourni par Microsoft.
+
+### Avantages
+- **Précision de reconnaissance**: La plus haute précision de reconnaissance
+- **Rapide**: Vitesse de traitement très rapide
+
+## hi
+
+Microsoft द्वारा प्रदान किया गया एक स्थानीय OCR मॉड्यूल।
+
+### लाभ
+- **पहचान सटीकता**: उच्चतम पहचान सटीकता का दावा करता है
+- **तेज़**: बहुत तेज़ प्रोसेसिंग गति
+
+## hu
+
+A Microsoft által biztosított helyi OCR modul.
+
+### Előnyök
+- **Felismerési pontosság**: A legjobb felismerési pontossággal rendelkezik
+- **Sebesség**: Nagyon gyors feldolgozási sebesség
+
+## id
+
+Modul OCR lokal yang disediakan oleh Microsoft.
+
+### Keuntungan
+- **Akurasi Pengenalan**: Memiliki akurasi pengenalan tertinggi
+- **Cepat**: Kecepatan pemrosesan sangat cepat
+
+## ko
+
+Microsoft가 제공하는 로컬 OCR 모듈입니다.
+
+### 장점
+- **인식 정확도**: 가장 높은 인식 정확도를 자랑합니다
+- **빠름**: 처리 속도가 매우 빠릅니다
+
+## ms
+
+Modul OCR tempatan yang disediakan oleh Microsoft.
+
+### Kelebihan
+- **Ketepatan Pengecaman**: Mempunyai ketepatan pengecaman tertinggi
+- **Pantas**: Kelajuan pemprosesan sangat pantas
+
+## pl
+
+Lokalny moduł OCR dostarczony przez Microsoft.
+
+### Zalety
+- **Dokładność rozpoznawania**: Może pochwalić się najwyższą dokładnością rozpoznawania
+- **Szybkość**: Bardzo szybka prędkość przetwarzania
+
+## pt-BR
+
+Módulo OCR local fornecido pela Microsoft.
+
+### Vantagens
+- **Precisão de reconhecimento**: Possui a maior precisão de reconhecimento
+- **Rápido**: Velocidade de processamento muito rápida
+
+## ru
+
+Локальный OCR-модуль, предоставляемый Microsoft.
+
+### Преимущества
+- **Точность распознавания**: Обладает наивысшей точностью распознавания
+- **Скорость**: Очень высокая скорость обработки
+
+## th
+
+โมดูล OCR ในเครื่องที่จัดทำโดย Microsoft
+
+### ข้อดี
+- **ความแม่นยำในการรู้จำ**: มีความแม่นยำในการรู้จำสูงสุด
+- **ความเร็ว**: ความเร็วในการประมวลผลสูงมาก
+
+## tr
+
+Microsoft tarafından sağlanan yerel bir OCR modülü.
+
+### Avantajlar
+- **Tanıma Doğruluğu**: En yüksek tanıma doğruluğuna sahiptir
+- **Hızlı**: Çok hızlı işleme hızı
+
+## vi
+
+Mô-đun OCR cục bộ do Microsoft cung cấp.
+
+### Ưu điểm
+- **Độ chính xác nhận dạng**: Có độ chính xác nhận dạng cao nhất
+- **Nhanh**: Tốc độ xử lý rất nhanh
+
+## zh-CN
+
+Microsoft 提供的本地 OCR 模块。
+
+### 优点
+- **识别准确度**:拥有最高的识别准确度
+- **快速**:处理速度非常快
+
+## zh-TW
+
+Microsoft 提供的本地 OCR 模組。
+
+### 優點
+- **識別準確度**:擁有最高的識別準確度
+- **快速**:處理速度非常快
diff --git a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/README.md b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/README.md
index 20cbada5..f1dd595c 100644
--- a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/README.md
@@ -1,10 +1,12 @@
# WindowTranslator PLaMo Translator Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) で、日本語に強いPLaMo 2 Translateモデルをローカル実行する翻訳プラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)で、日本語に強いPLaMo 2 Translateモデルをローカル実行する翻訳プラグインです。
## 機能
-- LLamaSharpを使用したローカル翻訳
+- LLamaSharpを使用した日本語に強いローカル翻訳
- 翻訳テキストを外部サービスへ送信せずに処理
- 初回利用時に量子化済みPLaMo翻訳モデルを自動取得
- コンテキスト長と使用するVRAM量を設定可能
@@ -18,8 +20,213 @@
モデル取得後の翻訳はローカルで実行されます。用語集と追加コンテキストには対応していません。
-## インストール
+## en
+
+A [WindowTranslator](https://github.com/Freeesia/WindowTranslator) translation plugin that runs the Japanese-focused PLaMo 2 Translate model locally.
+
+## Features
+
+- Japanese-focused local translation through LLamaSharp
+- Processes translated text without sending it to an external service
+- Downloads a quantized PLaMo translation model when it is first used
+- Configurable context length and VRAM usage
+
+## Requirements
+
+- 64-bit Windows
+- Sufficient free storage and memory
+- An NVIDIA GPU and driver with CUDA support are recommended
+- An internet connection when downloading the model for the first time
+
+After the model has been downloaded, translation runs locally. Glossaries and additional translation context are not supported.
+
+## ar
+
+وحدة ترجمة تستخدم LLM محلي متخصص في اليابانية.
+
+### المزايا
+- **متخصص في اليابانية**: محسن للترجمات اليابانية
+- **مجاني تماماً**: نموذج مفتوح المصدر بدون رسوم
+- **الخصوصية**: يعمل محلياً، البيانات لا تُرسل للخارج
+- **غير متصل**: لا حاجة لاتصال بالإنترنت
+
+## cs
+
+Překlad PLaMo
+
+- Délka kontextu: Větší hodnoty umožňují překládat delší texty.
+- Využití VRAM: Množství paměti GPU k použití. -1: pouze GPU. 0: pouze CPU. (Jednotka: GB)
+
+## de
+
+Ein Übersetzungsmodul, das lokales LLM spezialisiert für Japanisch verwendet.
+
+### Vorteile
+- **Japanisch-spezialisiert**: Optimiert für japanische Übersetzung
+- **Völlig kostenlos**: Open-Source-Modell ohne Gebühren
+- **Datenschutz**: Läuft lokal, Daten werden nicht extern gesendet
+- **Offline**: Keine Internetverbindung erforderlich
+
+## es
+
+Un módulo de traducción que utiliza un LLM local especializado para japonés.
+
+### Ventajas
+- **Especializado en japonés**: Optimizado para traducciones al japonés
+- **Completamente gratis**: Modelo de código abierto sin cargos
+- **Privacidad**: Funciona localmente, los datos no se envían al exterior
+- **Sin conexión**: No se necesita conexión a Internet
+
+## fa
+
+ماژول ترجمهای که از LLM محلی تخصصی در زبان ژاپنی استفاده میکند.
+
+### مزایا
+- **تخصصی در ژاپنی**: بهینهشده برای ترجمههای ژاپنی
+- **کاملاً رایگان**: مدل متنباز بدون هزینه
+- **حریم خصوصی**: به صورت محلی اجرا میشود، دادهها به خارج ارسال نمیشوند
+- **آفلاین**: نیاز به اتصال به اینترنت ندارد
+
+## fil
+
+Isang modyul ng pagsasalin na gumagamit ng local LLM na dalubhasa para sa wikang Hapon.
+
+### Mga Bentahe
+- **Dalubhasa sa Hapon**: Naka-optimize para sa pagsasalin ng Hapon
+- **Lubos na Libre**: Open source model na walang bayad
+- **Privacy**: Tumatakbo nang lokal, ang data ay hindi ipinapadala sa labas
+- **Offline**: Walang kailangang koneksyon sa internet
+
+## fr
+
+Un module de traduction utilisant un LLM local spécialisé pour le japonais.
+
+### Avantages
+- **Spécialisé japonais**: Optimisé pour les traductions japonaises
+- **Complètement gratuit**: Modèle open source sans frais
+- **Confidentialité**: Fonctionne localement, les données ne sont pas envoyées à l'extérieur
+- **Hors ligne**: Pas de connexion Internet nécessaire
+
+## hi
+
+जापानी भाषा के लिए विशेष स्थानीय LLM का उपयोग करने वाला अनुवाद मॉड्यूल।
+
+### फायदे
+- **जापानी विशेषज्ञता**: जापानी अनुवाद के लिए अनुकूलित
+- **पूर्ण रूप से निःशुल्क**: ओपन सोर्स मॉडल के साथ कोई शुल्क नहीं
+- **गोपनीयता**: स्थानीय रूप से चलता है, डेटा बाहर नहीं भेजा जाता
+- **ऑफ़लाइन**: इंटरनेट कनेक्शन की आवश्यकता नहीं
+
+## hu
+
+PLaMo fordítás
+
+- Kontextus hossza: A nagyobb értékek lehetővé teszik hosszabb szövegek fordítását.
+- VRAM használat: A használandó GPU memória mennyisége. -1: Csak GPU. 0: Csak CPU. (Egység: GB)
+
+## id
+
+Modul terjemahan menggunakan LLM lokal khusus untuk Bahasa Jepang.
+
+### Keuntungan
+- **Khusus Jepang**: Dioptimalkan untuk terjemahan Jepang
+- **Sepenuhnya Gratis**: Model sumber terbuka tanpa biaya
+- **Privasi**: Berjalan secara lokal, data tidak dikirim ke luar
+- **Offline**: Tidak ada koneksi internet yang diperlukan
+
+## ko
+
+일본어에 특화된 로컬 LLM을 사용하는 번역 모듈입니다.
+
+### 장점
+- **일본어 특화**: 일본어 번역에 최적화되어 있습니다
+- **완전 무료**: 오픈 소스 모델로 비용이 발생하지 않습니다
+- **프라이버시**: 로컬에서 작동하므로 데이터가 외부로 전송되지 않습니다
+- **오프라인**: 인터넷 연결이 필요 없습니다
+
+## ms
+
+Modul terjemahan menggunakan LLM tempatan khusus untuk Bahasa Jepun.
+
+### Kelebihan
+- **Khusus Jepun**: Dioptimumkan untuk terjemahan Jepun
+- **Percuma Sepenuhnya**: Model sumber terbuka tanpa caj
+- **Privasi**: Berjalan secara tempatan, data tidak dihantar ke luar
+- **Luar Talian**: Tiada sambungan internet diperlukan
+
+## pl
+
+Tłumaczenie PLaMo
+
+- Długość kontekstu: Większe wartości umożliwiają tłumaczenie dłuższych tekstów.
+- Użycie VRAM: Ilość pamięci GPU do użycia. -1: tylko GPU. 0: tylko CPU. (Jednostka: GB)
+
+## pt-BR
+
+Módulo de tradução que utiliza LLM local especializado em japonês.
+
+### Vantagens
+- **Especializado em japonês**: Otimizado para tradução de japonês
+- **Totalmente gratuito**: Modelo de código aberto sem custos
+- **Privacidade**: Como opera localmente, dados não são enviados externamente
+- **Offline**: Não requer conexão à internet
+
+## ru
+
+Модуль перевода, использующий локальный LLM, специализированный для японского языка.
+
+### Преимущества
+- **Специализация на японском**: Оптимизирован для перевода на японский язык
+- **Полностью бесплатно**: Модель с открытым исходным кодом без платежей
+- **Конфиденциальность**: Работает локально, данные не передаются наружу
+- **Автономный**: Не требуется подключение к интернету
+
+## th
+
+โมดูลการแปลที่ใช้ LLM ในเครื่องที่เชี่ยวชาญสำหรับภาษาญี่ปุ่น
+
+### ข้อดี
+- **เชี่ยวชาญภาษาญี่ปุ่น**: ปรับให้เหมาะสมสำหรับการแปลภาษาญี่ปุ่น
+- **ฟรีทั้งหมด**: โมเดลโอเพนซอร์สไม่มีค่าใช้จ่าย
+- **ความเป็นส่วนตัว**: ทำงานในเครื่อง ข้อมูลไม่ถูกส่งออกภายนอก
+- **ออฟไลน์**: ไม่ต้องการการเชื่อมต่ออินเทอร์เน็ต
+
+## tr
+
+Japonca için özelleştirilmiş yerel LLM kullanan bir çeviri modülü.
+
+### Avantajlar
+- **Japonca Uzmanlaşması**: Japonca çeviri için optimize edilmiş
+- **Tamamen Ücretsiz**: Açık kaynak modeli, ücret yok
+- **Gizlilik**: Yerel olarak çalışır, veriler dışarıya gönderilmez
+- **Çevrimdışı**: İnternet bağlantısı gerekmez
+
+## vi
+
+Mô-đun dịch thuật sử dụng LLM cục bộ chuyên về tiếng Nhật.
+
+### Ưu điểm
+- **Chuyên về tiếng Nhật**: Được tối ưu hóa cho dịch tiếng Nhật
+- **Hoàn toàn miễn phí**: Mô hình nguồn mở không tốn phí
+- **Quyền riêng tư**: Chạy cục bộ, dữ liệu không được gửi ra bên ngoài
+- **Ngoại tuyến**: Không cần kết nối internet
+
+## zh-CN
+
+使用专门针对日语的本地 LLM 的翻译模块。
+
+### 优点
+- **日语专用**:针对日语翻译进行了优化
+- **完全免费**:开源模型不产生费用
+- **隐私保护**:在本地运行,数据不会发送到外部
+- **离线**:无需互联网连接
+
+## zh-TW
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+使用專門針對日語的本地 LLM 的翻譯模組。
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+### 優點
+- **日語專用**:針對日語翻譯進行了最佳化
+- **完全免費**:開源模型不產生費用
+- **隱私保護**:在本地執行,資料不會傳送到外部
+- **離線**:無需網際網路連線
diff --git a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/README.md b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/README.md
index 8002e1f6..0aa2e338 100644
--- a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/README.md
+++ b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/README.md
@@ -1,10 +1,12 @@
# WindowTranslator Tesseract OCR Plugin
-[WindowTranslator](https://github.com/Freeesia/WindowTranslator) で、オープンソースのTesseract OCRエンジンを利用するプラグインです。
+## ja
+
+[WindowTranslator](https://github.com/Freeesia/WindowTranslator)で、オープンソースのTesseract OCRエンジンを利用するプラグインです。
## 機能
-- Tesseractによる多言語OCR
+- Tesseractによる100以上の言語に対応したOCR
- 翻訳元言語に対応する`traineddata`を初回利用時に自動取得
- OCR領域の結合、拡大率、明るさ、コントラストを考慮した後処理
@@ -13,10 +15,181 @@
- Microsoft Visual C++ 2015以降のx64ランタイム
- 言語データを初めて取得するときのインターネット接続
-必要なVisual C++ランタイムがない場合は、WindowTranslatorからインストールできます。言語データは`tesseract-ocr/tessdata_best`から取得されます。
+必要なVisual C++ランタイムがない場合は、WindowTranslatorから取得できます。言語データは`tesseract-ocr/tessdata_best`から取得されます。
+
+## en
+
+A [WindowTranslator](https://github.com/Freeesia/WindowTranslator) plugin that uses the open-source Tesseract OCR engine.
+
+## Features
+
+- Tesseract OCR with support for more than 100 languages
+- Downloads the `traineddata` for the translation source language when it is first used
+- Post-processing that accounts for merged OCR regions, scale, brightness, and contrast
+
+## Requirements
+
+- The x64 Microsoft Visual C++ 2015 or later runtime
+- An internet connection when downloading language data for the first time
+
+WindowTranslator can obtain the required Visual C++ runtime if it is missing. Language data is downloaded from `tesseract-ocr/tessdata_best`.
+
+## ar
+
+محرك OCR مفتوح المصدر.
+
+### المزايا
+- **دعم متعدد اللغات**: يدعم أكثر من 100 لغة
+- **الاستقرار**: محرك موثوق به مع تاريخ طويل
+
+## cs
+
+OCR engine s otevřeným zdrojovým kódem.
+
+### Výhody
+- **Vícejazyčná podpora**: Podporuje více než 100 jazyků
+- **Stabilita**: Spolehlivý engine s dlouhou historií
+
+## de
+
+Eine Open-Source-OCR-Engine.
+
+### Vorteile
+- **Mehrsprachige Unterstützung**: Unterstützt über 100 Sprachen
+- **Stabilität**: Zuverlässige Engine mit langer Geschichte
+
+## es
+
+Un motor OCR de código abierto.
+
+### Ventajas
+- **Soporte multilingüe**: Admite más de 100 idiomas
+- **Estabilidad**: Un motor confiable con una larga historia
+
+## fa
+
+موتور OCR متنباز.
+
+### مزایا
+- **پشتیبانی از چند زبان**: از بیش از 100 زبان پشتیبانی میکند
+- **پایداری**: موتور قابل اعتماد با سابقه طولانی
+
+## fil
+
+Isang open-source na OCR engine.
+
+### Mga Bentahe
+- **Suporta sa Maraming Wika**: Sumusuporta sa higit sa 100 wika
+- **Katatagan**: Maaasahang engine na may mahabang kasaysayan
+
+## fr
+
+Un moteur OCR open source.
+
+### Avantages
+- **Support multilingue**: Prend en charge plus de 100 langues
+- **Stabilité**: Un moteur fiable avec une longue histoire
+
+## hi
+
+एक ओपन-सोर्स OCR इंजन।
+
+### लाभ
+- **बहुभाषी समर्थन**: 100 से अधिक भाषाओं का समर्थन करता है
+- **स्थिरता**: लंबे इतिहास वाला विश्वसनीय इंजन
+
+## hu
+
+Nyílt forráskódú OCR motor.
+
+### Előnyök
+- **Többnyelvű támogatás**: Több mint 100 nyelvet támogat
+- **Stabilitás**: Hosszú múltra visszatekintő megbízható motor
+
+## id
+
+Mesin OCR sumber terbuka.
+
+### Keuntungan
+- **Dukungan Multibahasa**: Mendukung lebih dari 100 bahasa
+- **Stabilitas**: Mesin yang dapat diandalkan dengan sejarah panjang
+
+## ko
+
+오픈 소스 OCR 엔진입니다.
+
+### 장점
+- **다국어 지원**: 100개 이상의 언어를 지원합니다
+- **안정성**: 오랜 역사를 가진 신뢰할 수 있는 엔진
+
+## ms
+
+Enjin OCR sumber terbuka.
+
+### Kelebihan
+- **Sokongan Berbilang Bahasa**: Menyokong lebih 100 bahasa
+- **Kestabilan**: Enjin yang boleh dipercayai dengan sejarah panjang
+
+## pl
+
+Silnik OCR o otwartym kodzie źródłowym.
+
+### Zalety
+- **Wsparcie wielojęzyczne**: Obsługuje ponad 100 języków
+- **Stabilność**: Niezawodny silnik z długą historią
+
+## pt-BR
+
+Motor OCR de código aberto.
+
+### Vantagens
+- **Suporte multilíngue**: Suporta mais de 100 idiomas
+- **Estabilidade**: Motor confiável com longa história
+
+## ru
+
+OCR-движок с открытым исходным кодом.
+
+### Преимущества
+- **Многоязычная поддержка**: Поддерживает более 100 языков
+- **Стабильность**: Надежный движок с долгой историей
+
+## th
+
+เครื่องมือ OCR แบบโอเพนซอร์ส
+
+### ข้อดี
+- **รองรับหลายภาษา**: รองรับมากกว่า 100 ภาษา
+- **ความเสถียร**: เครื่องมือที่เชื่อถือได้พร้อมประวัติที่ยาวนาน
+
+## tr
+
+Açık kaynaklı bir OCR motoru.
+
+### Avantajlar
+- **Çok Dilli Destek**: 100'den fazla dili destekler
+- **Kararlılık**: Uzun geçmişe sahip güvenilir motor
+
+## vi
+
+Công cụ OCR nguồn mở.
+
+### Ưu điểm
+- **Hỗ trợ đa ngôn ngữ**: Hỗ trợ hơn 100 ngôn ngữ
+- **Ổn định**: Công cụ đáng tin cậy với lịch sử lâu dài
+
+## zh-CN
+
+开源 OCR 引擎。
+
+### 优点
+- **多语言支持**:支持100多种语言
+- **稳定性**:历史悠久的可靠引擎
-## インストール
+## zh-TW
-WindowTranslatorの設定画面で3番目の「プラグイン」タブを開き、このプラグインをインストールしてください。プレリリース版を利用する場合は、このプラグインの「プレリリース」にチェックを入れます。
+開源 OCR 引擎。
-インストールまたは更新の反映にはWindowTranslatorの再起動が必要です。
+### 優點
+- **多語言支援**:支援100多種語言
+- **穩定性**:歷史悠久的可靠引擎
diff --git a/WindowTranslator.Tests/LocalizedReadmeSelectorTests.cs b/WindowTranslator.Tests/LocalizedReadmeSelectorTests.cs
new file mode 100644
index 00000000..7cf8c5a4
--- /dev/null
+++ b/WindowTranslator.Tests/LocalizedReadmeSelectorTests.cs
@@ -0,0 +1,137 @@
+using System.Globalization;
+using WindowTranslator.Modules.PluginStore;
+
+namespace WindowTranslator.Tests;
+
+public class LocalizedReadmeSelectorTests
+{
+ [Theory]
+ [InlineData("pt-BR", "Português do Brasil")]
+ [InlineData("fr-CA", "Français")]
+ [InlineData("it-IT", "English")]
+ public void SelectUsesExactParentAndEnglishFallback(
+ string cultureName,
+ string expected)
+ {
+ const string markdown = """
+ ## pt-BR
+
+ Português do Brasil
+
+ ## fr
+
+ Français
+
+ ## en
+
+ English
+ """;
+
+ var result = LocalizedReadmeSelector.Select(
+ markdown,
+ CultureInfo.GetCultureInfo(cultureName));
+
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void SelectKeepsTheSharedPreambleAndOrdinaryHeadings()
+ {
+ const string markdown = """
+ # Shared title
+
+ Shared introduction.
+
+ ## ja
+
+ 日本語
+
+ ## 機能
+
+ - 機能A
+
+ ## en
+
+ English
+
+ ## Features
+
+ - Feature A
+ """;
+
+ var result = LocalizedReadmeSelector.Select(
+ markdown,
+ CultureInfo.GetCultureInfo("ja-JP"));
+
+ Assert.Contains("# Shared title", result);
+ Assert.Contains("## 機能", result);
+ Assert.Contains("- 機能A", result);
+ Assert.DoesNotContain("## ja", result);
+ Assert.DoesNotContain("English", result);
+ }
+
+ [Fact]
+ public void SelectFallsBackToTheFirstNonEmptyLanguageSection()
+ {
+ const string markdown = """
+ ## ja
+
+ 日本語
+
+ ## de
+
+ Deutsch
+ """;
+
+ var result = LocalizedReadmeSelector.Select(
+ markdown,
+ CultureInfo.GetCultureInfo("fr-FR"));
+
+ Assert.Equal("日本語", result);
+ }
+
+ [Fact]
+ public void SelectReturnsTheOriginalMarkdownWithoutLanguageSections()
+ {
+ const string markdown = """
+ # README
+
+ ## Features
+
+ - Feature A
+ """;
+
+ var result = LocalizedReadmeSelector.Select(
+ markdown,
+ CultureInfo.GetCultureInfo("en-US"));
+
+ Assert.Same(markdown, result);
+ }
+
+ [Fact]
+ public void SelectKeepsAnUppercaseAcronymHeadingInsideTheLanguageSection()
+ {
+ const string markdown = """
+ ## ja
+
+ 日本語
+
+ ## API
+
+ APIの説明
+
+ ## en
+
+ English
+ """;
+
+ var result = LocalizedReadmeSelector.Select(
+ markdown,
+ CultureInfo.GetCultureInfo("ja-JP"));
+
+ Assert.Contains("## API", result);
+ Assert.Contains("APIの説明", result);
+ Assert.DoesNotContain("English", result);
+ }
+
+}
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index 54ae0b60..7582ce46 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -1249,6 +1249,55 @@ public async Task SelectedPackageLoadsReadmeForTheSelectedReleaseChannel()
}
}
+ [Fact]
+ public async Task PackageReadmeUsesTheRequestedUiCulture()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Localized.Plugin",
+ "1.0.0",
+ CreatePackage(
+ "Localized.Plugin",
+ "1.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/net10.0/Localized.Plugin.dll"] = "plugin"u8.ToArray(),
+ ["README.md"] = """
+ ## ja
+
+ # 日本語
+
+ ## en
+
+ # English
+ """u8.ToArray(),
+ }));
+ handler.AddReadmeUrl(
+ "Localized.Plugin",
+ "1.0.0",
+ "https://nuget.test/readme/localized.plugin/1.0.0");
+ using var service = CreateService(handler, testDirectory);
+
+ var readme = await service.GetPackageReadmeAsync(
+ "Localized.Plugin",
+ "1.0.0",
+ CultureInfo.GetCultureInfo("en-US"));
+
+ Assert.Equal("# English", readme);
+ Assert.DoesNotContain(
+ handler.RequestedPaths,
+ path => path.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase));
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
[Fact]
public void StartupCleanupDeletesOnlyPackagesMissingFromAReadableManifest()
{
diff --git a/WindowTranslator/Modules/PluginStore/LocalizedReadmeSelector.cs b/WindowTranslator/Modules/PluginStore/LocalizedReadmeSelector.cs
new file mode 100644
index 00000000..0f565f6a
--- /dev/null
+++ b/WindowTranslator/Modules/PluginStore/LocalizedReadmeSelector.cs
@@ -0,0 +1,97 @@
+using System.Globalization;
+using System.Text.RegularExpressions;
+
+namespace WindowTranslator.Modules.PluginStore;
+
+internal static partial class LocalizedReadmeSelector
+{
+ public static string Select(string markdown, CultureInfo culture)
+ {
+ ArgumentNullException.ThrowIfNull(markdown);
+ ArgumentNullException.ThrowIfNull(culture);
+
+ var headings = LanguageHeadingRegex.Matches(markdown)
+ .Select(TryCreateHeading)
+ .OfType()
+ .ToArray();
+ if (headings.Length == 0)
+ {
+ return markdown;
+ }
+
+ var preamble = markdown[..headings[0].Start];
+ var sections = headings
+ .Select((heading, index) => new ReadmeSection(
+ heading.CultureName,
+ markdown[heading.ContentStart..(index + 1 < headings.Length
+ ? headings[index + 1].Start
+ : markdown.Length)]))
+ .Where(section => !string.IsNullOrWhiteSpace(section.Content))
+ .ToArray();
+ if (sections.Length == 0)
+ {
+ return markdown;
+ }
+
+ var selected = GetPreferredCultureNames(culture)
+ .Select(name => sections.FirstOrDefault(section =>
+ section.CultureName.Equals(name, StringComparison.OrdinalIgnoreCase)))
+ .FirstOrDefault(section => section is not null)
+ ?? sections[0];
+ return Combine(preamble, selected.Content, markdown);
+ }
+
+ private static IEnumerable GetPreferredCultureNames(CultureInfo culture)
+ {
+ for (var candidate = culture; !string.IsNullOrEmpty(candidate.Name); candidate = candidate.Parent)
+ {
+ yield return candidate.Name;
+ }
+
+ yield return "en";
+ }
+
+ private static LanguageHeading? TryCreateHeading(Match match)
+ {
+ var value = match.Groups["culture"].Value;
+ try
+ {
+ var cultureName = CultureInfo.GetCultureInfo(value).Name;
+ return value.Equals(cultureName, StringComparison.Ordinal)
+ ? new(match.Index, match.Index + match.Length, cultureName)
+ : null;
+ }
+ catch (CultureNotFoundException)
+ {
+ return null;
+ }
+ }
+
+ private static string Combine(string preamble, string content, string markdown)
+ {
+ var shared = preamble.TrimEnd('\r', '\n');
+ var localized = content.Trim('\r', '\n');
+ if (shared.Length == 0)
+ {
+ return localized;
+ }
+ if (localized.Length == 0)
+ {
+ return shared;
+ }
+
+ var newLine = markdown.Contains("\r\n", StringComparison.Ordinal)
+ ? "\r\n"
+ : "\n";
+ return $"{shared}{newLine}{newLine}{localized}";
+ }
+
+ [GeneratedRegex(
+ @"^##[ \t]+(?[A-Za-z]{2,3}(?:-[A-Za-z0-9]{2,8})*)(?:[ \t]+#+)?[ \t]*(?:\r?\n|$)",
+ RegexOptions.CultureInvariant | RegexOptions.Multiline)]
+ private static partial Regex LanguageHeadingRegex { get; }
+
+ private sealed record LanguageHeading(int Start, int ContentStart, string CultureName);
+
+ private sealed record ReadmeSection(string CultureName, string Content);
+}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index 45db7eba..a771f0c7 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
@@ -198,10 +199,7 @@ internal async Task RefreshPackageInformationAsync(CancellationToken cancellatio
///
/// 指定したパッケージバージョンのREADMEを取得します。
///
- public async Task GetPackageReadmeAsync(
- string packageId,
- string version,
- CancellationToken cancellationToken = default)
+ public async Task GetPackageReadmeAsync(string packageId, string version, CultureInfo culture, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(packageId))
{
@@ -211,6 +209,7 @@ internal async Task RefreshPackageInformationAsync(CancellationToken cancellatio
{
throw new ArgumentException($"不正なNuGetパッケージバージョンです: {version}", nameof(version));
}
+ ArgumentNullException.ThrowIfNull(culture);
var metadataResource = await this.repository
.GetResourceAsync(cancellationToken)
@@ -235,7 +234,10 @@ internal async Task RefreshPackageInformationAsync(CancellationToken cancellatio
}
response.EnsureSuccessStatusCode();
- return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
+ var markdown = await response.Content
+ .ReadAsStringAsync(cancellationToken)
+ .ConfigureAwait(false);
+ return LocalizedReadmeSelector.Select(markdown, culture);
}
///
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index 25d1ce77..cc2e9d07 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
+using System.Globalization;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
@@ -339,6 +340,7 @@ private async Task LoadPackageReadmeAsync(
var readme = await this.nugetService.GetPackageReadmeAsync(
package.Id,
version,
+ CultureInfo.CurrentUICulture,
cancellationSource.Token).ConfigureAwait(true);
if (!cancellationSource.IsCancellationRequested
&& ReferenceEquals(package, this.SelectedPackage)
diff --git a/docs/plugin.md b/docs/plugin.md
index 7fccac4a..18c0c6e3 100644
--- a/docs/plugin.md
+++ b/docs/plugin.md
@@ -31,12 +31,15 @@ cd WindowTranslator.Plugin.YourPlugin
YourName
プラグインストアに表示する具体的な説明文
https://github.com/YourName/YourPlugin
+ README.md
$(PackageTags);windowtranslator-plugin
MIT
+
+
@@ -58,6 +61,48 @@ cd WindowTranslator.Plugin.YourPlugin
> WindowTranslator との互換性判定に使用されます。サポートする最も古い
> `WindowTranslator.Abstractions` のバージョンを指定してください。
+### README の多言語化
+
+プラグインストアは、README 内のカルチャー名だけで構成された第2レベル見出しを
+言語セクションとして認識します。README は1ファイルのまま、必要な言語だけを
+次のように記載してください。
+
+```markdown
+# Your Plugin
+
+## ja
+
+日本語の説明です。
+
+## 機能
+
+- 機能A
+
+## en
+
+English description.
+
+## Features
+
+- Feature A
+```
+
+アプリのUIカルチャーとの完全一致、親言語、英語、先頭セクションの順で表示する
+言語を選択します。`## 機能`や`## Features`など、カルチャー名ではない見出しは
+言語セクション内の通常の見出しとして扱われます。カルチャー名は`ja`、`pt-BR`、
+`zh-Hans`のように、.NETの正規表記と大文字・小文字まで一致させてください。
+正規形のカルチャー名を持つ第2レベル見出しはREADME全体で言語境界として扱うため、
+コード例など別の用途には使用しないでください。
+
+リポジトリ内の公式プラグインは、既存のUI翻訳に合わせて次の22言語を1つの
+READMEに収録しています。
+
+`ja`、`en`、`ar`、`cs`、`de`、`es`、`fa`、`fil`、`fr`、`hi`、`hu`、`id`、
+`ko`、`ms`、`pl`、`pt-BR`、`ru`、`th`、`tr`、`vi`、`zh-CN`、`zh-TW`
+
+外部プラグインで全言語を用意する必要はありません。収録していないUI言語には
+英語セクションが表示されます。
+
### 3. プラグインを実装
対象のインターフェースを実装します:
From 727f966d26676de02bd3d509ffec3d8dba8af9d5 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Fri, 14 Aug 2026 03:13:01 +0900
Subject: [PATCH 32/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=81=AE=E9=85=8D=E5=B8=83=E8=A8=AD=E5=AE=9A=E3=82=92?=
=?UTF-8?q?=E3=83=97=E3=83=AD=E3=83=91=E3=83=86=E3=82=A3=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/dotnet-desktop.yml | 31 +--
.github/workflows/dotnet-package.yml | 31 +--
Plugins/Directory.Build.props | 6 +-
Plugins/Directory.Build.targets | 10 +-
...tor.Plugin.BergamotTranslatorPlugin.csproj | 2 +
...wTranslator.Plugin.ColorThiefPlugin.csproj | 2 +
...nslator.Plugin.DeepLTranslatePlugin.csproj | 2 +-
...WindowTranslator.Plugin.DummyPlugin.csproj | 2 +-
.../WindowTranslator.Plugin.FoMPlugin.csproj | 2 +-
...anslator.Plugin.GitHubCopilotPlugin.csproj | 2 +-
...dowTranslator.Plugin.GoogleAIPlugin.csproj | 2 +-
...lator.Plugin.GoogleAppsSctiptPlugin.csproj | 2 +-
.../WindowTranslator.Plugin.LLMPlugin.csproj | 2 +-
...indowTranslator.Plugin.OneOcrPlugin.csproj | 2 +
...WindowTranslator.Plugin.PLaMoPlugin.csproj | 2 +-
...ranslator.Plugin.TesseractOCRPlugin.csproj | 2 +-
Plugins/WindowTranslator.Plugins.proj | 28 +++
.../NuGetPluginServiceTests.cs | 188 +-------------
WindowTranslator.slnx | 1 +
.../Modules/PluginStore/NuGetPluginCatalog.cs | 9 +-
.../PluginStore/NuGetPluginOperation.cs | 232 ++----------------
.../Modules/PluginStore/NuGetPluginService.cs | 34 ++-
22 files changed, 132 insertions(+), 462 deletions(-)
create mode 100644 Plugins/WindowTranslator.Plugins.proj
diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml
index 95f68e4c..e742efde 100644
--- a/.github/workflows/dotnet-desktop.yml
+++ b/.github/workflows/dotnet-desktop.yml
@@ -134,27 +134,16 @@ jobs:
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- run: |
- $projects = Get-ChildItem -Path "Plugins\WindowTranslator.Plugin.*\*.csproj" |
- Where-Object { $_.Directory.Name -notmatch "Dummy" -and $_.Directory.Name -notmatch "Tests?" }
- foreach ($project in $projects) {
- $propertyOutput = dotnet msbuild $project.FullName -nologo -getProperty:ExcludeFromAppBundle
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
- $excludeFromAppBundle = ($propertyOutput | Select-Object -Last 1).Trim()
- if ($excludeFromAppBundle -eq "true") {
- Write-Host "Skip app bundle: $($project.Name)"
- continue
- }
- dotnet publish $project.FullName -c Release -o "publish\plugins\$($project.Directory.Name)" `
- -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
- -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
- -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
- -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} `
- -p:DecryptKey="${{ secrets.WINDOWTRANSLATOR_DECRYPTKEY }}"
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
+ dotnet msbuild Plugins\WindowTranslator.Plugins.proj -target:PublishAppBundle `
+ -p:Configuration=Release `
+ -p:AppBundleOutputPath="$((Resolve-Path publish).Path)\plugins" `
+ -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
+ -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
+ -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
+ -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} `
+ -p:DecryptKey="${{ secrets.WINDOWTRANSLATOR_DECRYPTKEY }}"
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
}
- uses: actions/download-artifact@v8
if: ${{ needs.docs.result == 'success' }}
diff --git a/.github/workflows/dotnet-package.yml b/.github/workflows/dotnet-package.yml
index de573cbd..aadba2c7 100644
--- a/.github/workflows/dotnet-package.yml
+++ b/.github/workflows/dotnet-package.yml
@@ -63,27 +63,16 @@ jobs:
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
- $projects = Get-ChildItem Plugins\WindowTranslator.Plugin.*\*.csproj |
- Where-Object { $_.Directory.Name -notlike '*.Tests' }
- foreach ($project in $projects) {
- $propertyOutput = dotnet msbuild $project.FullName -nologo -getProperty:IsPackable
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
- $isPackable = ($propertyOutput | Select-Object -Last 1).Trim()
- if ($isPackable -ne "true") {
- Write-Host "Skip NuGet package: $($project.Name)"
- continue
- }
- dotnet pack $project.FullName -c Release -o pack `
- -p:Version=${{ steps.package-version.outputs.version }} `
- -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
- -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
- -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} `
- -p:DecryptKey="${{ secrets.WINDOWTRANSLATOR_DECRYPTKEY }}"
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
+ dotnet msbuild Plugins\WindowTranslator.Plugins.proj -target:PackPlugins `
+ -p:Configuration=Release `
+ -p:PackageOutputPath="$((Resolve-Path pack).Path)" `
+ -p:Version=${{ steps.package-version.outputs.version }} `
+ -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
+ -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
+ -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} `
+ -p:DecryptKey="${{ secrets.WINDOWTRANSLATOR_DECRYPTKEY }}"
+ if ($LASTEXITCODE -ne 0) {
+ exit $LASTEXITCODE
}
- name: NuGet login
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
diff --git a/Plugins/Directory.Build.props b/Plugins/Directory.Build.props
index e50f2937..ce5e1e56 100644
--- a/Plugins/Directory.Build.props
+++ b/Plugins/Directory.Build.props
@@ -6,12 +6,12 @@
true
-
+
+ false
+ false
false
-
- false
diff --git a/Plugins/Directory.Build.targets b/Plugins/Directory.Build.targets
index 7371bc28..314aa32c 100644
--- a/Plugins/Directory.Build.targets
+++ b/Plugins/Directory.Build.targets
@@ -1,4 +1,12 @@
+
+ $(PublishToNuGet)
+ $(IncludeInAppBundle)
+ $(AppBundleOutputPath)\$(MSBuildProjectName)\
+
+
@@ -39,7 +47,7 @@
-
+
diff --git a/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj b/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj
index 379a3053..8170af1c 100644
--- a/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj
@@ -4,6 +4,8 @@
net10.0
Bergamot Translator Plugin
Offline neural machine translation for WindowTranslator using Bergamot.
+ true
+ true
enable
enable
true
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj b/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj
index 9515f260..9ae8011c 100644
--- a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj
@@ -3,6 +3,8 @@
net10.0-windows10.0.20348.0
ColorThief Plugin
Detects readable foreground and background colors for translated text.
+ true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
index ecff13ec..2fef9710 100644
--- a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
@@ -3,7 +3,7 @@
DeepL Translator Plugin
Translation for WindowTranslator using the DeepL API.
- true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj b/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj
index 7fb550e8..79f9d404 100644
--- a/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj
@@ -2,7 +2,7 @@
- false
+ false
diff --git a/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj b/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
index c7979cc5..20a0a5ce 100644
--- a/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
@@ -4,7 +4,7 @@
Fields of Mistria Filter Plugin
Context-aware translation filtering for Fields of Mistria.
true
- true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
index 71a02d16..7a48c626 100644
--- a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
@@ -3,7 +3,7 @@
net10.0-windows10.0.20348.0
GitHub Copilot Translator Plugin
Translation for WindowTranslator using GitHub Copilot.
- true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
index ffbfadf7..91850c72 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
@@ -4,7 +4,7 @@
net10.0-windows10.0.20348.0
Google AI Plugin
Translation, OCR, and text correction for WindowTranslator using Google AI.
- true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
index a7f4954b..65e944fa 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
@@ -2,7 +2,7 @@
Google Apps Script Translator Plugin
Translation for WindowTranslator through Google Apps Script.
- true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj b/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
index 6bac6e23..c6bb66ee 100644
--- a/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
@@ -3,7 +3,7 @@
net10.0-windows10.0.20348.0
LLM Plugin
Translation, OCR, and text correction through OpenAI-compatible language models.
- true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj
index 97ac11cd..d89bcd5b 100644
--- a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj
@@ -4,6 +4,8 @@
OneOCR Plugin
OCR for WindowTranslator using the Windows OneOCR engine.
true
+ true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
index 7c88752d..8ddea9c2 100644
--- a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
@@ -4,7 +4,7 @@
net10.0
PLaMo Translator Plugin
Local PLaMo translation for WindowTranslator using LLamaSharp and CUDA.
- true
+ true
true
x64
diff --git a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
index ba85c1f4..142137d7 100644
--- a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
@@ -4,7 +4,7 @@
net10.0-windows10.0.20348.0
Tesseract OCR Plugin
OCR for WindowTranslator using the Tesseract engine.
- true
+ true
diff --git a/Plugins/WindowTranslator.Plugins.proj b/Plugins/WindowTranslator.Plugins.proj
new file mode 100644
index 00000000..a11ff9c3
--- /dev/null
+++ b/Plugins/WindowTranslator.Plugins.proj
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index 7582ce46..cf5afba4 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -1128,7 +1128,7 @@ public async Task InstallRejectsPackageWithoutPluginTag()
var targetDirectory = Path.Combine(testDirectory, "Root.Plugin");
Directory.CreateDirectory(targetDirectory);
File.WriteAllText(Path.Combine(targetDirectory, "plugin.txt"), "old");
- await NuGetPluginOperation.SaveManifestAsync(
+ await NuGetPluginService.SaveManifestAsync(
Path.Combine(testDirectory, "nuget-manifest.json"),
new([new(
"Root.Plugin",
@@ -1299,19 +1299,19 @@ public async Task PackageReadmeUsesTheRequestedUiCulture()
}
[Fact]
- public void StartupCleanupDeletesOnlyPackagesMissingFromAReadableManifest()
+ public void StartupCleanupDeletesDirectoriesMissingFromAReadableManifest()
{
var sourceDirectory = CreateTestDirectory();
try
{
var installedDirectory = Path.Combine(sourceDirectory, "Installed.Plugin");
var removedDirectory = Path.Combine(sourceDirectory, "Removed.Plugin");
- var operationsDirectory = Path.Combine(
+ var interruptedInstallDirectory = Path.Combine(
sourceDirectory,
- NuGetPluginOperation.OperationsDirectoryName);
+ "Installed.Plugin.installing-test");
Directory.CreateDirectory(installedDirectory);
Directory.CreateDirectory(removedDirectory);
- Directory.CreateDirectory(operationsDirectory);
+ Directory.CreateDirectory(interruptedInstallDirectory);
File.WriteAllText(
Path.Combine(sourceDirectory, "nuget-manifest.json"),
JsonSerializer.Serialize(
@@ -1329,7 +1329,7 @@ public void StartupCleanupDeletesOnlyPackagesMissingFromAReadableManifest()
Assert.True(Directory.Exists(installedDirectory));
Assert.False(Directory.Exists(removedDirectory));
- Assert.True(Directory.Exists(operationsDirectory));
+ Assert.False(Directory.Exists(interruptedInstallDirectory));
var directoryKeptForInvalidManifest = Path.Combine(
sourceDirectory,
@@ -1367,11 +1367,6 @@ public void CatalogSynchronizationCopiesOnlyChangesAndRemovesStaleFiles()
Path.Combine(sourceDirectory, "Root.Plugin", "Unchanged.dll");
File.WriteAllText(unchangedSourcePath, "unchanged");
Directory.CreateDirectory(Path.Combine(sourceDirectory, "Empty.Plugin"));
- Directory.CreateDirectory(Path.Combine(sourceDirectory, ".operations"));
- Directory.CreateDirectory(Path.Combine(sourceDirectory, ".operations", "backup-test"));
- File.WriteAllText(
- Path.Combine(sourceDirectory, ".operations", "backup-test", "old.dll"),
- "old");
Directory.CreateDirectory(Path.Combine(sourceDirectory, "Root.Plugin.backup-test"));
File.WriteAllText(
Path.Combine(sourceDirectory, "Root.Plugin.backup-test", "old.dll"),
@@ -1445,8 +1440,6 @@ public void CatalogSynchronizationCopiesOnlyChangesAndRemovesStaleFiles()
Assert.False(File.Exists(Path.Combine(destinationDirectory, "nuget-manifest.json")));
Assert.False(File.Exists(
Path.Combine(destinationDirectory, "nuget-manifest.json.tmp-test")));
- Assert.False(Directory.Exists(
- Path.Combine(destinationDirectory, ".operations")));
Assert.False(Directory.Exists(
Path.Combine(destinationDirectory, "Root.Plugin.backup-test")));
Assert.False(Directory.Exists(
@@ -1488,171 +1481,6 @@ public void CatalogSynchronizationClearsStaleFilesWhenSourceIsMissing()
}
}
- [Fact]
- public async Task InterruptedInstallIsRolledBackBeforeItIsTreatedAsCompleted()
- {
- var sourceDirectory = CreateTestDirectory();
- try
- {
- const string packageId = "Root.Plugin";
- var originalManifest = new InstalledManifest(
- [new InstalledPackageInfo(
- packageId,
- "1.0.0",
- HostMajorVersion: 1,
- AbstractionsVersionRange: "(, )")]);
- var updatedManifest = new InstalledManifest(
- [new InstalledPackageInfo(
- packageId,
- "2.0.0",
- HostMajorVersion: 1,
- AbstractionsVersionRange: "(, )")]);
- var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
- await NuGetPluginOperation.SaveManifestAsync(
- manifestPath,
- originalManifest,
- CancellationToken.None);
-
- var operation = await NuGetPluginOperation.BeginAsync(
- sourceDirectory,
- packageId,
- originalManifest,
- CancellationToken.None);
- Directory.CreateDirectory(operation.BackupPath);
- File.WriteAllText(Path.Combine(operation.BackupPath, "plugin.txt"), "old");
- Directory.Move(operation.WorkingPath, operation.TargetPath);
- File.WriteAllText(Path.Combine(operation.TargetPath, "plugin.txt"), "new");
- await NuGetPluginOperation.SaveManifestAsync(
- manifestPath,
- updatedManifest,
- CancellationToken.None);
-
- var unresolved = await NuGetPluginOperation.RecoverInterruptedOperationsAsync(
- sourceDirectory);
-
- Assert.Empty(unresolved);
- Assert.Equal("old", File.ReadAllText(Path.Combine(operation.TargetPath, "plugin.txt")));
- var restoredManifest = JsonSerializer.Deserialize(
- File.ReadAllText(manifestPath),
- NuGetPluginService.ManifestJsonOptions);
- Assert.Equal("1.0.0", Assert.Single(restoredManifest!.Packages).Version);
- Assert.False(File.Exists(operation.PendingPath));
- Assert.False(Directory.Exists(operation.WorkingPath));
- Assert.False(Directory.Exists(operation.BackupPath));
- }
- finally
- {
- DeleteTestDirectory(sourceDirectory);
- }
- }
-
- [Fact]
- public async Task CompletedInstallKeepsNewFilesAndOnlyCleansOperationData()
- {
- var sourceDirectory = CreateTestDirectory();
- try
- {
- const string packageId = "Root.Plugin";
- var originalManifest = new InstalledManifest(
- [new InstalledPackageInfo(
- packageId,
- "1.0.0",
- HostMajorVersion: 1,
- AbstractionsVersionRange: "(, )")]);
- var updatedManifest = new InstalledManifest(
- [new InstalledPackageInfo(
- packageId,
- "2.0.0",
- HostMajorVersion: 1,
- AbstractionsVersionRange: "(, )")]);
- var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
- await NuGetPluginOperation.SaveManifestAsync(
- manifestPath,
- originalManifest,
- CancellationToken.None);
-
- var operation = await NuGetPluginOperation.BeginAsync(
- sourceDirectory,
- packageId,
- originalManifest,
- CancellationToken.None);
- Directory.CreateDirectory(operation.BackupPath);
- File.WriteAllText(Path.Combine(operation.BackupPath, "plugin.txt"), "old");
- Directory.Move(operation.WorkingPath, operation.TargetPath);
- File.WriteAllText(Path.Combine(operation.TargetPath, "plugin.txt"), "new");
- await NuGetPluginOperation.SaveManifestAsync(
- manifestPath,
- updatedManifest,
- CancellationToken.None);
- operation.Commit();
-
- var unresolved = await NuGetPluginOperation.RecoverInterruptedOperationsAsync(
- sourceDirectory);
-
- Assert.Empty(unresolved);
- Assert.Equal("new", File.ReadAllText(Path.Combine(operation.TargetPath, "plugin.txt")));
- var retainedManifest = JsonSerializer.Deserialize(
- File.ReadAllText(manifestPath),
- NuGetPluginService.ManifestJsonOptions);
- Assert.Equal("2.0.0", Assert.Single(retainedManifest!.Packages).Version);
- Assert.False(File.Exists(operation.PendingPath));
- Assert.False(File.Exists(operation.CommittedPath));
- Assert.False(Directory.Exists(operation.BackupPath));
- }
- finally
- {
- DeleteTestDirectory(sourceDirectory);
- }
- }
-
- [Fact]
- public async Task CompletedInstallRemainsLoadableWhenOperationCleanupFails()
- {
- var sourceDirectory = CreateTestDirectory();
- try
- {
- const string packageId = "Root.Plugin";
- var manifest = new InstalledManifest(
- [new InstalledPackageInfo(
- packageId,
- "2.0.0",
- HostMajorVersion: 1,
- AbstractionsVersionRange: "(, )")]);
- var manifestPath = Path.Combine(sourceDirectory, "nuget-manifest.json");
- var operation = await NuGetPluginOperation.BeginAsync(
- sourceDirectory,
- packageId,
- originalManifest: null,
- CancellationToken.None);
- Directory.Move(operation.WorkingPath, operation.TargetPath);
- File.WriteAllText(Path.Combine(operation.TargetPath, "plugin.txt"), "new");
- await NuGetPluginOperation.SaveManifestAsync(
- manifestPath,
- manifest,
- CancellationToken.None);
- operation.Commit();
-
- Directory.CreateDirectory(operation.BackupPath);
- var lockedPath = Path.Combine(operation.BackupPath, "locked.txt");
- await File.WriteAllTextAsync(lockedPath, "locked");
- await using (File.Open(lockedPath, FileMode.Open, FileAccess.Read, FileShare.None))
- {
- var unresolved = await NuGetPluginOperation.RecoverInterruptedOperationsAsync(
- sourceDirectory);
-
- Assert.Empty(unresolved);
- Assert.Equal("new", File.ReadAllText(Path.Combine(operation.TargetPath, "plugin.txt")));
- }
-
- Assert.Empty(await NuGetPluginOperation.RecoverInterruptedOperationsAsync(sourceDirectory));
- Assert.False(Directory.Exists(operation.BackupPath));
- }
- finally
- {
- DeleteTestDirectory(sourceDirectory);
- }
- }
-
[Fact]
public void CatalogSynchronizationFollowsCompatibilityValidationSetting()
{
@@ -1796,7 +1624,7 @@ public async Task CatalogLoadsARealAssemblyFromAPackageSubdirectory()
packageDirectory,
"*.deps.json",
SearchOption.AllDirectories));
- await NuGetPluginOperation.SaveManifestAsync(
+ await NuGetPluginService.SaveManifestAsync(
Path.Combine(sourceDirectory, "nuget-manifest.json"),
new([new(
"Catalog.Probe",
@@ -1876,7 +1704,7 @@ public async Task CatalogLoadsTheSatelliteAssemblyForTheRequestedCulture()
"WindowTranslator.Tests.resources.dll"),
Path.Combine(cultureDirectory, "WindowTranslator.Tests.resources.dll"));
}
- await NuGetPluginOperation.SaveManifestAsync(
+ await NuGetPluginService.SaveManifestAsync(
Path.Combine(sourceDirectory, "nuget-manifest.json"),
new([new(
"Catalog.Probe",
diff --git a/WindowTranslator.slnx b/WindowTranslator.slnx
index 3447197f..4756c92a 100644
--- a/WindowTranslator.slnx
+++ b/WindowTranslator.slnx
@@ -25,6 +25,7 @@
+
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 4c37a1e5..240241dc 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -60,15 +60,11 @@ internal NuGetPluginCatalog(
///
public async Task Initialize()
{
- var unresolvedOperations = await NuGetPluginOperation
- .RecoverInterruptedOperationsAsync(this.sourceDir)
- .ConfigureAwait(false);
DeleteUninstalledPackageDirectories(this.sourceDir);
var loadablePackages = GetLoadablePackageIds(
this.sourceDir,
this.hostMajorVersion,
this.hostAbstractionsVersion);
- loadablePackages.ExceptWith(unresolvedOperations);
SynchronizePluginFiles(this.sourceDir, this.tempDir, loadablePackages);
this.innerCatalog = CreateCatalog(this.tempDir, this.options);
@@ -381,10 +377,7 @@ internal static void DeleteUninstalledPackageDirectories(string sourceDirectory)
foreach (var packageDirectory in Directory.EnumerateDirectories(sourceDirectory))
{
var directoryName = Path.GetFileName(packageDirectory);
- if (directoryName.Equals(
- NuGetPluginOperation.OperationsDirectoryName,
- StringComparison.OrdinalIgnoreCase)
- || installedPackageIds.Contains(directoryName))
+ if (installedPackageIds.Contains(directoryName))
{
continue;
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs
index b8fdaaf4..2ab0936b 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginOperation.cs
@@ -1,240 +1,54 @@
using System.Diagnostics;
using System.IO;
-using System.Text.Json;
-using System.Text.Json.Serialization;
using NuGet.Packaging;
namespace WindowTranslator.Modules.PluginStore;
-internal sealed record NuGetPluginOperationState(
- [property: JsonRequired] string PackageId,
- InstalledManifest? OriginalManifest);
-
-internal sealed class NuGetPluginOperation : IAsyncDisposable
+internal sealed class NuGetPluginOperation : IDisposable
{
- internal const string OperationsDirectoryName = ".operations";
- private const string PendingFileName = "pending.json";
- private const string CommittedFileName = "committed.json";
-
- private readonly string rootDirectory;
- private readonly string operationDirectory;
- private readonly NuGetPluginOperationState state;
private bool committed;
- private NuGetPluginOperation(
- string rootDirectory,
- string operationDirectory,
- NuGetPluginOperationState state)
+ internal NuGetPluginOperation(string rootDirectory, string packageId)
{
- this.rootDirectory = Path.GetFullPath(rootDirectory);
- this.operationDirectory = Path.GetFullPath(operationDirectory);
- this.state = state;
- _ = this.TargetPath;
+ PackageIdValidator.ValidatePackageId(packageId);
+ var rootPath = Path.GetFullPath(rootDirectory);
+ var operationId = Guid.NewGuid().ToString("N");
+ this.TargetPath = Path.Combine(rootPath, packageId);
+ this.WorkingPath = Path.Combine(rootPath, $"{packageId}.installing-{operationId}");
+ this.BackupPath = Path.Combine(rootPath, $"{packageId}.backup-{operationId}");
+ Directory.CreateDirectory(this.WorkingPath);
}
- internal string TargetPath => GetPackageDirectory(this.rootDirectory, this.state.PackageId);
-
- internal string WorkingPath => Path.Combine(this.operationDirectory, "working");
-
- internal string BackupPath => Path.Combine(this.operationDirectory, "backup");
-
- internal string PendingPath => Path.Combine(this.operationDirectory, PendingFileName);
-
- internal string CommittedPath => Path.Combine(this.operationDirectory, CommittedFileName);
-
- internal static async Task BeginAsync(
- string rootDirectory,
- string packageId,
- InstalledManifest? originalManifest,
- CancellationToken cancellationToken)
- {
- var operationDirectory = Path.Combine(
- Path.GetFullPath(rootDirectory),
- OperationsDirectoryName,
- Guid.NewGuid().ToString("N"));
- var operation = new NuGetPluginOperation(
- rootDirectory,
- operationDirectory,
- new(packageId, originalManifest));
- Directory.CreateDirectory(operationDirectory);
- Directory.CreateDirectory(operation.WorkingPath);
+ internal string TargetPath { get; }
- try
- {
- await SaveJsonAsync(
- operation.PendingPath,
- operation.state,
- cancellationToken).ConfigureAwait(false);
- return operation;
- }
- catch
- {
- DeleteDirectoryIfExists(operationDirectory);
- throw;
- }
- }
-
- internal static string GetPackageDirectory(string rootDirectory, string packageId)
- {
- PackageIdValidator.ValidatePackageId(packageId);
- return Path.Combine(Path.GetFullPath(rootDirectory), packageId);
- }
+ internal string WorkingPath { get; }
- internal static Task SaveManifestAsync(
- string manifestPath,
- InstalledManifest manifest,
- CancellationToken cancellationToken)
- => SaveJsonAsync(manifestPath, manifest, cancellationToken);
+ internal string BackupPath { get; }
- internal void Commit()
- {
- File.Move(this.PendingPath, this.CommittedPath);
- this.committed = true;
- }
+ internal void Commit() => this.committed = true;
- public async ValueTask DisposeAsync()
+ public void Dispose()
{
try
{
- if (this.committed)
+ if (!this.committed)
{
- CleanupCommitted();
- }
- else
- {
- await RollbackAsync(CancellationToken.None).ConfigureAwait(false);
- CleanupRolledBack();
- }
- }
- catch (Exception ex)
- {
- Trace.TraceWarning(
- "NuGetプラグイン操作の後処理に失敗しました: {0} {1} ({2})",
- this.state.PackageId,
- Path.GetFileName(this.operationDirectory),
- ex);
- }
- }
-
- internal static async Task> RecoverInterruptedOperationsAsync(
- string rootDirectory,
- CancellationToken cancellationToken = default)
- {
- var operationsDirectory = Path.Combine(
- Path.GetFullPath(rootDirectory),
- OperationsDirectoryName);
- if (!Directory.Exists(operationsDirectory))
- {
- return new HashSet(StringComparer.OrdinalIgnoreCase);
- }
-
- var unresolvedPackageIds = new HashSet(StringComparer.OrdinalIgnoreCase);
- foreach (var operationDirectory in Directory.EnumerateDirectories(operationsDirectory))
- {
- cancellationToken.ThrowIfCancellationRequested();
- NuGetPluginOperationState? state = null;
- var isCommitted = false;
- try
- {
- var committedPath = Path.Combine(operationDirectory, CommittedFileName);
- isCommitted = File.Exists(committedPath);
- var statePath = isCommitted
- ? committedPath
- : Path.Combine(operationDirectory, PendingFileName);
- if (!File.Exists(statePath))
+ if (!Directory.Exists(this.WorkingPath) && Directory.Exists(this.TargetPath))
{
- DeleteDirectoryIfExists(operationDirectory);
- continue;
+ Directory.Move(this.TargetPath, this.WorkingPath);
}
-
- state = JsonSerializer.Deserialize(
- await File.ReadAllTextAsync(statePath, cancellationToken).ConfigureAwait(false),
- NuGetPluginService.ManifestJsonOptions)
- ?? throw new InvalidDataException("NuGetプラグイン操作情報が空です。");
- var operation = new NuGetPluginOperation(rootDirectory, operationDirectory, state)
+ if (Directory.Exists(this.BackupPath))
{
- committed = isCommitted,
- };
- if (isCommitted)
- {
- operation.CleanupCommitted();
- }
- else
- {
- await operation.RollbackAsync(cancellationToken).ConfigureAwait(false);
- operation.CleanupRolledBack();
+ Directory.Move(this.BackupPath, this.TargetPath);
}
}
- catch (Exception ex) when (ex is not OperationCanceledException)
- {
- if (!isCommitted && !string.IsNullOrWhiteSpace(state?.PackageId))
- {
- unresolvedPackageIds.Add(state.PackageId);
- }
- Trace.TraceWarning(
- "NuGetプラグイン操作の復旧に失敗しました: {0} ({1})",
- operationDirectory,
- ex);
- }
- }
-
- return unresolvedPackageIds;
- }
- private async Task RollbackAsync(CancellationToken cancellationToken)
- {
- if (!Directory.Exists(this.WorkingPath) && Directory.Exists(this.TargetPath))
- {
- Directory.Move(this.TargetPath, this.WorkingPath);
- }
- if (Directory.Exists(this.BackupPath))
- {
- Directory.Move(this.BackupPath, this.TargetPath);
+ DeleteDirectoryIfExists(this.WorkingPath);
+ DeleteDirectoryIfExists(this.BackupPath);
}
-
- var manifestPath = Path.Combine(this.rootDirectory, "nuget-manifest.json");
- if (this.state.OriginalManifest is { } originalManifest)
- {
- await SaveManifestAsync(
- manifestPath,
- originalManifest,
- cancellationToken).ConfigureAwait(false);
- }
- else if (File.Exists(manifestPath))
- {
- File.Delete(manifestPath);
- }
- }
-
- private void CleanupCommitted()
- => DeleteDirectoryIfExists(this.operationDirectory);
-
- private void CleanupRolledBack()
- {
- // 復旧後に同じ操作を再実行しないよう、作業データより先に状態を消す。
- File.Delete(this.PendingPath);
- CleanupCommitted();
- }
-
- private static async Task SaveJsonAsync(
- string destinationPath,
- T value,
- CancellationToken cancellationToken)
- {
- Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
- var temporaryPath = $"{destinationPath}.tmp-{Guid.NewGuid():N}";
- try
- {
- var json = JsonSerializer.Serialize(value, NuGetPluginService.ManifestJsonOptions);
- await File.WriteAllTextAsync(
- temporaryPath,
- json,
- cancellationToken).ConfigureAwait(false);
- File.Move(temporaryPath, destinationPath, overwrite: true);
- }
- finally
+ catch (Exception ex)
{
- File.Delete(temporaryPath);
+ Trace.TraceWarning("NuGet plugin operation cleanup failed: {0} ({1})", this.TargetPath, ex);
}
}
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index a771f0c7..ac0e1fb3 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -246,13 +246,8 @@ internal async Task RefreshPackageInformationAsync(CancellationToken cancellatio
public async Task InstallPackageAsync(string packageId, string version, IProgress? progress = null, CancellationToken cancellationToken = default)
{
using var operation = await this.operationLock.EnterAsync(cancellationToken);
- var manifestExisted = File.Exists(this.manifestPath);
var currentManifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
- await using var pluginOperation = await NuGetPluginOperation.BeginAsync(
- this.nugetPluginsDir,
- packageId,
- manifestExisted ? currentManifest : null,
- cancellationToken).ConfigureAwait(false);
+ using var pluginOperation = new NuGetPluginOperation(this.nugetPluginsDir, packageId);
var packageResource = await this.repository
.GetResourceAsync(cancellationToken)
@@ -483,10 +478,29 @@ private async Task LoadManifestAsync(CancellationToken cancel
}
private Task SaveManifestAsync(InstalledManifest manifest, CancellationToken cancellationToken)
- => NuGetPluginOperation.SaveManifestAsync(
- this.manifestPath,
- manifest,
- cancellationToken);
+ => SaveManifestAsync(this.manifestPath, manifest, cancellationToken);
+
+ internal static async Task SaveManifestAsync(
+ string manifestPath,
+ InstalledManifest manifest,
+ CancellationToken cancellationToken)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(manifestPath)!);
+ var temporaryPath = $"{manifestPath}.tmp-{Guid.NewGuid():N}";
+ try
+ {
+ var json = JsonSerializer.Serialize(manifest, ManifestJsonOptions);
+ await File.WriteAllTextAsync(
+ temporaryPath,
+ json,
+ cancellationToken).ConfigureAwait(false);
+ File.Move(temporaryPath, manifestPath, overwrite: true);
+ }
+ finally
+ {
+ File.Delete(temporaryPath);
+ }
+ }
public override void Dispose()
{
From e550bbb16fa802d9c5a3299a688a8afff4ac1e04 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Fri, 14 Aug 2026 17:31:59 +0900
Subject: [PATCH 33/43] =?UTF-8?q?=E9=85=8D=E5=B8=83=E3=83=97=E3=83=AD?=
=?UTF-8?q?=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E3=81=AE=E8=A8=AD=E5=AE=9A?=
=?UTF-8?q?=E3=82=92=E6=A8=99=E6=BA=96=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/dotnet-desktop.yml | 7 +---
.github/workflows/dotnet-package.yml | 39 ++----------------
ColorThief/ColorThief/ColorThief.csproj | 2 +
Plugins/Directory.Build.props | 17 ++++----
Plugins/Directory.Build.targets | 25 ++---------
...tor.Plugin.BergamotTranslatorPlugin.csproj | 4 +-
...wTranslator.Plugin.ColorThiefPlugin.csproj | 4 +-
...nslator.Plugin.DeepLTranslatePlugin.csproj | 3 +-
...WindowTranslator.Plugin.DummyPlugin.csproj | 3 +-
.../WindowTranslator.Plugin.FoMPlugin.csproj | 3 +-
...anslator.Plugin.GitHubCopilotPlugin.csproj | 3 +-
...dowTranslator.Plugin.GoogleAIPlugin.csproj | 3 +-
...lator.Plugin.GoogleAppsSctiptPlugin.csproj | 3 +-
.../WindowTranslator.Plugin.LLMPlugin.csproj | 3 +-
...indowTranslator.Plugin.OneOcrPlugin.csproj | 4 +-
...WindowTranslator.Plugin.PLaMoPlugin.csproj | 3 +-
...ranslator.Plugin.TesseractOCRPlugin.csproj | 3 +-
Plugins/WindowTranslator.Plugins.proj | 28 -------------
.../WindowTranslator.Abstractions.csproj | 1 +
WindowTranslator.Packages.proj | 30 ++++++++++++++
.../ColorThiefModuleTests.cs | 0
...lator.Plugin.ColorThiefPlugin.Tests.csproj | 7 +++-
.../images/text_000.jpg | Bin
.../images/text_001.jpg | Bin
.../images/text_002.jpg | Bin
.../images/text_003.jpg | Bin
.../images/text_004.jpg | Bin
.../images/text_005.jpg | Bin
.../images/text_006.jpg | Bin
.../images/text_007.jpg | Bin
.../images/text_008.jpg | Bin
.../images/text_009.jpg | Bin
.../images/text_010.jpg | Bin
.../images/text_011.jpg | Bin
.../images/text_012.jpg | Bin
.../images/text_013.jpg | Bin
.../images/text_014.jpg | Bin
.../images/text_015.jpg | Bin
.../images/text_016.jpg | Bin
WindowTranslator.slnx | 4 +-
40 files changed, 82 insertions(+), 117 deletions(-)
delete mode 100644 Plugins/WindowTranslator.Plugins.proj
create mode 100644 WindowTranslator.Packages.proj
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/ColorThiefModuleTests.cs (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/WindowTranslator.Plugin.ColorThiefPlugin.Tests.csproj (65%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_000.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_001.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_002.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_003.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_004.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_005.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_006.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_007.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_008.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_009.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_010.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_011.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_012.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_013.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_014.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_015.jpg (100%)
rename {Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests => WindowTranslator.Plugin.ColorThiefPlugin.Tests}/images/text_016.jpg (100%)
diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml
index e742efde..093f16fb 100644
--- a/.github/workflows/dotnet-desktop.yml
+++ b/.github/workflows/dotnet-desktop.yml
@@ -134,17 +134,14 @@ jobs:
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- run: |
- dotnet msbuild Plugins\WindowTranslator.Plugins.proj -target:PublishAppBundle `
+ dotnet msbuild WindowTranslator.Packages.proj -target:PublishAppBundle `
-p:Configuration=Release `
- -p:AppBundleOutputPath="$((Resolve-Path publish).Path)\plugins" `
+ -p:AppBundleOutputPath="$PWD\publish\plugins" `
-p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} `
-p:DecryptKey="${{ secrets.WINDOWTRANSLATOR_DECRYPTKEY }}"
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
- uses: actions/download-artifact@v8
if: ${{ needs.docs.result == 'success' }}
with:
diff --git a/.github/workflows/dotnet-package.yml b/.github/workflows/dotnet-package.yml
index aadba2c7..7c512ac6 100644
--- a/.github/workflows/dotnet-package.yml
+++ b/.github/workflows/dotnet-package.yml
@@ -29,51 +29,18 @@ jobs:
versionSpec: "6.x"
- id: gitversion
uses: gittools/actions/gitversion/execute@v4.7.0
- - id: package-version
- shell: pwsh
- run: |
- if ('${{ github.event_name }}' -eq 'push') {
- $tag = '${{ github.ref_name }}'
- if ($tag -notmatch '^v(?[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?)$') {
- Write-Error "NuGet package tag must be v-prefixed SemVer: $tag"
- exit 1
- }
- "version=$($Matches.version)" >> $env:GITHUB_OUTPUT
- }
- else {
- "version=0.0.0-pr.${{ github.run_number }}" >> $env:GITHUB_OUTPUT
- }
- uses: Jimver/cuda-toolkit@v0.2.30
with:
cuda: '12.9.0'
- run: |
- dotnet pack WindowTranslator.Abstractions -c Release -o pack `
- -p:Version=${{ steps.package-version.outputs.version }} `
- -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
- -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
- -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
- dotnet pack ColorThief\ColorThief -c Release -o pack `
- -p:Version=${{ steps.package-version.outputs.version }} `
- -p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
- -p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
- -p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }}
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
- dotnet msbuild Plugins\WindowTranslator.Plugins.proj -target:PackPlugins `
+ dotnet msbuild WindowTranslator.Packages.proj -target:Pack `
-p:Configuration=Release `
- -p:PackageOutputPath="$((Resolve-Path pack).Path)" `
- -p:Version=${{ steps.package-version.outputs.version }} `
+ -p:PackageOutputPath="$PWD\pack" `
+ -p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
-p:AssemblyVersion=${{ steps.gitversion.outputs.assemblySemVer }} `
-p:FileVersion=${{ steps.gitversion.outputs.assemblySemFileVer }} `
-p:InformationalVersion=${{ steps.gitversion.outputs.informationalVersion }} `
-p:DecryptKey="${{ secrets.WINDOWTRANSLATOR_DECRYPTKEY }}"
- if ($LASTEXITCODE -ne 0) {
- exit $LASTEXITCODE
- }
- name: NuGet login
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
id: nuget_login
diff --git a/ColorThief/ColorThief/ColorThief.csproj b/ColorThief/ColorThief/ColorThief.csproj
index bf9a9540..2c3c567c 100644
--- a/ColorThief/ColorThief/ColorThief.csproj
+++ b/ColorThief/ColorThief/ColorThief.csproj
@@ -6,6 +6,8 @@
WindowTranslator ColorThief Support Library
Color extraction support library for the WindowTranslator ColorThief plugin.
$(PackageTags);ColorThief
+ true
+ false
enable
enable
StudioFreesia.ColorThief
diff --git a/Plugins/Directory.Build.props b/Plugins/Directory.Build.props
index ce5e1e56..81d8978b 100644
--- a/Plugins/Directory.Build.props
+++ b/Plugins/Directory.Build.props
@@ -4,15 +4,18 @@
net10.0
true
+ $(PackageTags);windowtranslator-plugin
+ README.md
+ plugin-icon.png
-
-
- false
- false
- false
-
+
+
+
+
+
+
+
diff --git a/Plugins/Directory.Build.targets b/Plugins/Directory.Build.targets
index 314aa32c..3244b5a0 100644
--- a/Plugins/Directory.Build.targets
+++ b/Plugins/Directory.Build.targets
@@ -1,38 +1,19 @@
-
- $(PublishToNuGet)
- $(IncludeInAppBundle)
- $(AppBundleOutputPath)\$(MSBuildProjectName)\
-
-
-
- $(PackageTags);windowtranslator-plugin
- README.md
- plugin-icon.png
+
$(TargetsForTfmSpecificBuildOutput);AddPluginRuntimeAssetsToPackage
-
-
-
-
-
-
-
-
<_PluginWinX64RuntimeAsset Include="$(TargetDir)runtimes\win-x64\**\*" />
@@ -47,7 +28,7 @@
-
+
diff --git a/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj b/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj
index 8170af1c..a9ee952e 100644
--- a/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.BergamotTranslatorPlugin/WindowTranslator.Plugin.BergamotTranslatorPlugin.csproj
@@ -4,8 +4,8 @@
net10.0
Bergamot Translator Plugin
Offline neural machine translation for WindowTranslator using Bergamot.
- true
- true
+ true
+ true
enable
enable
true
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj b/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj
index 9ae8011c..3e49347c 100644
--- a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.ColorThiefPlugin/WindowTranslator.Plugin.ColorThiefPlugin.csproj
@@ -3,8 +3,8 @@
net10.0-windows10.0.20348.0
ColorThief Plugin
Detects readable foreground and background colors for translated text.
- true
- true
+ true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
index 2fef9710..2afc9efa 100644
--- a/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.DeepLTranslatePlugin/WindowTranslator.Plugin.DeepLTranslatePlugin.csproj
@@ -3,7 +3,8 @@
DeepL Translator Plugin
Translation for WindowTranslator using the DeepL API.
- true
+ true
+ false
diff --git a/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj b/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj
index 79f9d404..e37e8a3a 100644
--- a/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.DummyPlugin/WindowTranslator.Plugin.DummyPlugin.csproj
@@ -2,7 +2,8 @@
- false
+ false
+ false
diff --git a/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj b/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
index 20a0a5ce..79cd0353 100644
--- a/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.FoMPlugin/WindowTranslator.Plugin.FoMPlugin.csproj
@@ -4,7 +4,8 @@
Fields of Mistria Filter Plugin
Context-aware translation filtering for Fields of Mistria.
true
- true
+ true
+ false
diff --git a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
index 7a48c626..db0736dd 100644
--- a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
@@ -3,7 +3,8 @@
net10.0-windows10.0.20348.0
GitHub Copilot Translator Plugin
Translation for WindowTranslator using GitHub Copilot.
- true
+ true
+ false
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
index 91850c72..4a7f97ba 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GoogleAIPlugin/WindowTranslator.Plugin.GoogleAIPlugin.csproj
@@ -4,7 +4,8 @@
net10.0-windows10.0.20348.0
Google AI Plugin
Translation, OCR, and text correction for WindowTranslator using Google AI.
- true
+ true
+ false
diff --git a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
index 65e944fa..c317da57 100644
--- a/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GoogleAppsSctiptPlugin/WindowTranslator.Plugin.GoogleAppsSctiptPlugin.csproj
@@ -2,7 +2,8 @@
Google Apps Script Translator Plugin
Translation for WindowTranslator through Google Apps Script.
- true
+ true
+ false
diff --git a/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj b/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
index c6bb66ee..a426d83c 100644
--- a/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.LLMPlugin/WindowTranslator.Plugin.LLMPlugin.csproj
@@ -3,7 +3,8 @@
net10.0-windows10.0.20348.0
LLM Plugin
Translation, OCR, and text correction through OpenAI-compatible language models.
- true
+ true
+ false
diff --git a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj
index d89bcd5b..010991cf 100644
--- a/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.OneOcrPlugin/WindowTranslator.Plugin.OneOcrPlugin.csproj
@@ -4,8 +4,8 @@
OneOCR Plugin
OCR for WindowTranslator using the Windows OneOCR engine.
true
- true
- true
+ true
+ true
diff --git a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
index 8ddea9c2..e95baa06 100644
--- a/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.PLaMoPlugin/WindowTranslator.Plugin.PLaMoPlugin.csproj
@@ -4,7 +4,8 @@
net10.0
PLaMo Translator Plugin
Local PLaMo translation for WindowTranslator using LLamaSharp and CUDA.
- true
+ true
+ false
true
x64
diff --git a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
index 142137d7..89d62cfd 100644
--- a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
@@ -4,7 +4,8 @@
net10.0-windows10.0.20348.0
Tesseract OCR Plugin
OCR for WindowTranslator using the Tesseract engine.
- true
+ true
+ false
diff --git a/Plugins/WindowTranslator.Plugins.proj b/Plugins/WindowTranslator.Plugins.proj
deleted file mode 100644
index a11ff9c3..00000000
--- a/Plugins/WindowTranslator.Plugins.proj
+++ /dev/null
@@ -1,28 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/WindowTranslator.Abstractions/WindowTranslator.Abstractions.csproj b/WindowTranslator.Abstractions/WindowTranslator.Abstractions.csproj
index db4086ad..d168a920 100644
--- a/WindowTranslator.Abstractions/WindowTranslator.Abstractions.csproj
+++ b/WindowTranslator.Abstractions/WindowTranslator.Abstractions.csproj
@@ -4,6 +4,7 @@
net10.0;net10.0-windows10.0.20348.0
WindowTranslator
true
+ false
wt.png
README.md
true
diff --git a/WindowTranslator.Packages.proj b/WindowTranslator.Packages.proj
new file mode 100644
index 00000000..65798a99
--- /dev/null
+++ b/WindowTranslator.Packages.proj
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/ColorThiefModuleTests.cs b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/ColorThiefModuleTests.cs
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/ColorThiefModuleTests.cs
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/ColorThiefModuleTests.cs
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/WindowTranslator.Plugin.ColorThiefPlugin.Tests.csproj b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/WindowTranslator.Plugin.ColorThiefPlugin.Tests.csproj
similarity index 65%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/WindowTranslator.Plugin.ColorThiefPlugin.Tests.csproj
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/WindowTranslator.Plugin.ColorThiefPlugin.Tests.csproj
index f4f0a345..33cb0650 100644
--- a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/WindowTranslator.Plugin.ColorThiefPlugin.Tests.csproj
+++ b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/WindowTranslator.Plugin.ColorThiefPlugin.Tests.csproj
@@ -1,6 +1,9 @@
net10.0-windows10.0.20348.0
+ true
+ false
+ false
@@ -15,8 +18,8 @@
-
-
+
+
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_000.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_000.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_000.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_000.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_001.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_001.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_001.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_001.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_002.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_002.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_002.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_002.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_003.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_003.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_003.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_003.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_004.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_004.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_004.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_004.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_005.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_005.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_005.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_005.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_006.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_006.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_006.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_006.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_007.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_007.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_007.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_007.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_008.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_008.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_008.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_008.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_009.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_009.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_009.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_009.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_010.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_010.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_010.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_010.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_011.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_011.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_011.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_011.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_012.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_012.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_012.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_012.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_013.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_013.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_013.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_013.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_014.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_014.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_014.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_014.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_015.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_015.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_015.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_015.jpg
diff --git a/Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_016.jpg b/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_016.jpg
similarity index 100%
rename from Plugins/WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_016.jpg
rename to WindowTranslator.Plugin.ColorThiefPlugin.Tests/images/text_016.jpg
diff --git a/WindowTranslator.slnx b/WindowTranslator.slnx
index 4756c92a..67a29b21 100644
--- a/WindowTranslator.slnx
+++ b/WindowTranslator.slnx
@@ -10,6 +10,7 @@
+
@@ -25,9 +26,7 @@
-
-
@@ -42,6 +41,7 @@
+
From be312a0c1797fc43847e687f96dad45a5c9fd54b Mon Sep 17 00:00:00 2001
From: Freesia
Date: Fri, 14 Aug 2026 17:37:03 +0900
Subject: [PATCH 34/43] =?UTF-8?q?=E9=9B=86=E7=B4=84=E3=83=97=E3=83=AD?=
=?UTF-8?q?=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E3=81=AB=E3=82=88=E3=82=8B?=
=?UTF-8?q?=E5=BE=A9=E5=85=83=E7=AB=B6=E5=90=88=E3=82=92=E5=9B=9E=E9=81=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/dotnet-desktop.yml | 2 +-
.github/workflows/dotnet-package.yml | 2 +-
.../WindowTranslator.Plugins.proj | 6 +++---
WindowTranslator.slnx | 2 +-
4 files changed, 6 insertions(+), 6 deletions(-)
rename WindowTranslator.Packages.proj => Plugins/WindowTranslator.Plugins.proj (73%)
diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml
index 093f16fb..24eaf959 100644
--- a/.github/workflows/dotnet-desktop.yml
+++ b/.github/workflows/dotnet-desktop.yml
@@ -134,7 +134,7 @@ jobs:
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
- run: |
- dotnet msbuild WindowTranslator.Packages.proj -target:PublishAppBundle `
+ dotnet msbuild Plugins\WindowTranslator.Plugins.proj -target:PublishAppBundle `
-p:Configuration=Release `
-p:AppBundleOutputPath="$PWD\publish\plugins" `
-p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
diff --git a/.github/workflows/dotnet-package.yml b/.github/workflows/dotnet-package.yml
index 7c512ac6..a0dacd41 100644
--- a/.github/workflows/dotnet-package.yml
+++ b/.github/workflows/dotnet-package.yml
@@ -33,7 +33,7 @@ jobs:
with:
cuda: '12.9.0'
- run: |
- dotnet msbuild WindowTranslator.Packages.proj -target:Pack `
+ dotnet msbuild Plugins\WindowTranslator.Plugins.proj -target:Pack `
-p:Configuration=Release `
-p:PackageOutputPath="$PWD\pack" `
-p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
diff --git a/WindowTranslator.Packages.proj b/Plugins/WindowTranslator.Plugins.proj
similarity index 73%
rename from WindowTranslator.Packages.proj
rename to Plugins/WindowTranslator.Plugins.proj
index 65798a99..f6ac1589 100644
--- a/WindowTranslator.Packages.proj
+++ b/Plugins/WindowTranslator.Plugins.proj
@@ -1,8 +1,8 @@
-
-
-
+
+
+
diff --git a/WindowTranslator.slnx b/WindowTranslator.slnx
index 67a29b21..2f4bab73 100644
--- a/WindowTranslator.slnx
+++ b/WindowTranslator.slnx
@@ -10,7 +10,6 @@
-
@@ -26,6 +25,7 @@
+
From cdaad3ee0465607da7e9e50470d58c486748e2b5 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Fri, 14 Aug 2026 17:46:33 +0900
Subject: [PATCH 35/43] =?UTF-8?q?=E9=85=8D=E5=B8=83=E3=83=97=E3=83=AD?=
=?UTF-8?q?=E3=82=B8=E3=82=A7=E3=82=AF=E3=83=88=E3=81=AE=E5=BE=A9=E5=85=83?=
=?UTF-8?q?=E7=AB=B6=E5=90=88=E3=82=92=E9=98=B2=E6=AD=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Plugins/WindowTranslator.Plugins.proj | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/Plugins/WindowTranslator.Plugins.proj b/Plugins/WindowTranslator.Plugins.proj
index f6ac1589..62e6d85a 100644
--- a/Plugins/WindowTranslator.Plugins.proj
+++ b/Plugins/WindowTranslator.Plugins.proj
@@ -8,8 +8,7 @@
+ Targets="Restore" />
From 03aee766cf5d43f40fafcb7c607f772da5bc9380 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Fri, 14 Aug 2026 20:41:32 +0900
Subject: [PATCH 36/43] =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=81=AE=E3=83=90?=
=?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=89=E5=87=A6=E7=90=86=E3=82=92=E5=85=83?=
=?UTF-8?q?=E3=81=AE=E5=AE=9F=E8=A3=85=E3=81=AB=E6=88=BB=E3=81=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../UserSettingsConfigurationTests.cs | 71 ----------------
WindowTranslator/Program.cs | 81 ++-----------------
2 files changed, 6 insertions(+), 146 deletions(-)
delete mode 100644 WindowTranslator.Tests/UserSettingsConfigurationTests.cs
diff --git a/WindowTranslator.Tests/UserSettingsConfigurationTests.cs b/WindowTranslator.Tests/UserSettingsConfigurationTests.cs
deleted file mode 100644
index b599fe26..00000000
--- a/WindowTranslator.Tests/UserSettingsConfigurationTests.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.Logging.Abstractions;
-using WindowTranslator.Modules;
-using WindowTranslator.Stores;
-
-namespace WindowTranslator.Tests;
-
-public sealed class UserSettingsConfigurationTests
-{
- [Fact]
- public void UserSettingsIgnoresPluginParametersThatHaveNoLoadedType()
- {
- var configuration = new ConfigurationBuilder()
- .AddInMemoryCollection(new Dictionary
- {
- ["Common:ViewMode"] = nameof(ViewMode.Capture),
- ["Common:HidePluginStoreDisclaimer"] = "true",
- ["Targets::Font"] = "Default Font",
- ["Targets:game:Font"] = "Test Font",
- ["Targets:game:SelectedPlugins:ITranslateModule"] = "MissingTranslator",
- ["Targets:game:PluginParams:MissingOptions:ApiKey"] = "secret",
- })
- .Build();
- var settings = new UserSettings();
-
- new global::ConfigureUserSettings(configuration).Configure(settings);
-
- Assert.Equal(ViewMode.Capture, settings.Common.ViewMode);
- Assert.True(settings.Common.HidePluginStoreDisclaimer);
- Assert.Equal("Default Font", settings.Targets[string.Empty].Font);
- var target = settings.Targets["game"];
- Assert.Equal("Test Font", target.Font);
- Assert.Equal(
- "MissingTranslator",
- target.SelectedPlugins[nameof(ITranslateModule)]);
- Assert.Empty(target.PluginParams);
- }
-
- [Fact]
- public void InvalidLoadedPluginParameterIsIgnoredWithoutChangingDefaults()
- {
- var configuration = new ConfigurationBuilder()
- .AddInMemoryCollection(new Dictionary
- {
- ["Targets:game:PluginParams:InvalidPluginParam:RetryCount"] =
- "not-an-integer",
- })
- .Build();
- var options = new InvalidPluginParam();
- var configure = new global::ConfigurePluginParam(
- configuration,
- new TestProcessInfoStore("game"),
- NullLogger>.Instance);
-
- configure.Configure(options);
-
- Assert.Equal(7, options.RetryCount);
- }
-
- public sealed class InvalidPluginParam : IPluginParam
- {
- public int RetryCount { get; set; } = 7;
- }
-
- private sealed class TestProcessInfoStore(string name) : IProcessInfoStore
- {
- public IntPtr MainWindowHandle => IntPtr.Zero;
-
- public string Name { get; } = name;
- }
-}
diff --git a/WindowTranslator/Program.cs b/WindowTranslator/Program.cs
index 8b27fdc3..5b3fcc32 100644
--- a/WindowTranslator/Program.cs
+++ b/WindowTranslator/Program.cs
@@ -196,7 +196,7 @@
AppInfo.Instance.Version.Major))
.AddHostedService(sp => sp.GetRequiredService());
builder.Services.AddTransient();
-builder.Services.AddTransient, ConfigureUserSettings>();
+builder.Services.Configure(builder.Configuration, op => op.ErrorOnUnknownConfiguration = false);
builder.Services.Configure(builder.Configuration.GetSection(nameof(UserSettings.Common)));
builder.Services.AddTransient(typeof(IConfigureNamedOptions<>), typeof(ConfigurePluginParam<>));
builder.Services.AddTransient(typeof(IConfigureOptions<>), typeof(ConfigurePluginParam<>));
@@ -264,23 +264,11 @@ static string GetPluginName(PluginNameOptions options, Type type)
}
}
-class ConfigureUserSettings(IConfiguration configuration) : IConfigureOptions
-{
- private readonly IConfiguration configuration = configuration;
-
- public void Configure(UserSettings options)
- => PluginParameterIgnoringConfigurationBinder.Bind(this.configuration, options);
-}
-
-class ConfigurePluginParam(
- IConfiguration configuration,
- IProcessInfoStore store,
- ILogger> logger) : IConfigureNamedOptions
+class ConfigurePluginParam(IConfiguration configuration, IProcessInfoStore store) : IConfigureNamedOptions
where TOptions : class, IPluginParam
{
private readonly IConfiguration configuration = configuration.GetSection(nameof(UserSettings.Targets));
private readonly IProcessInfoStore store = store;
- private readonly ILogger> logger = logger;
public void Configure(TOptions options)
{
@@ -289,7 +277,7 @@ public void Configure(TOptions options)
{
section = this.configuration.GetSection(Options.DefaultName);
}
- this.BindParameter(section, options);
+ GetTargetSection(section, typeof(TOptions).Name).Bind(options);
}
public void Configure(string? name, TOptions options)
@@ -300,46 +288,7 @@ public void Configure(string? name, TOptions options)
{
section = this.configuration.GetSection(Options.DefaultName);
}
- this.BindParameter(section, options);
- }
-
- private void BindParameter(IConfigurationSection targetSection, TOptions options)
- {
- var parameterSection = GetTargetSection(targetSection, typeof(TOptions).Name);
- if (!parameterSection.Exists())
- {
- return;
- }
-
- try
- {
- var configured = parameterSection.Get();
- if (configured is null)
- {
- return;
- }
-
- foreach (var property in typeof(TOptions).GetProperties(BindingFlags.Instance | BindingFlags.Public))
- {
- if (property.CanRead
- && property.CanWrite
- && property.GetIndexParameters().Length == 0)
- {
- property.SetValue(options, property.GetValue(configured));
- }
- }
- }
- catch (Exception ex) when (ex is InvalidOperationException
- or FormatException
- or NotSupportedException
- or MissingMethodException
- or ArgumentException
- or TargetInvocationException)
- {
- this.logger.LogWarning(
- "プラグインパラメータ {ParameterType} を読み込めないため無視します。",
- typeof(TOptions).Name);
- }
+ GetTargetSection(section, typeof(TOptions).Name).Bind(options);
}
private static IConfigurationSection GetTargetSection(IConfigurationSection section, string name)
@@ -367,7 +316,7 @@ public void Configure(TargetSettings options)
{
section = this.configuration.GetSection(Options.DefaultName);
}
- PluginParameterIgnoringConfigurationBinder.Bind(section, options);
+ section.Bind(options);
}
public void Configure(string? name, TargetSettings options)
@@ -378,25 +327,7 @@ public void Configure(string? name, TargetSettings options)
{
section = this.configuration.GetSection(Options.DefaultName);
}
- PluginParameterIgnoringConfigurationBinder.Bind(section, options);
- }
-}
-
-static class PluginParameterIgnoringConfigurationBinder
-{
- public static void Bind(IConfiguration configuration, object options)
- {
- var values = configuration
- .AsEnumerable(makePathsRelative: true)
- .Where(value => !value.Key
- .Split(ConfigurationPath.KeyDelimiter, StringSplitOptions.None)
- .Contains(
- nameof(TargetSettings.PluginParams),
- StringComparer.OrdinalIgnoreCase));
- var filteredConfiguration = new ConfigurationBuilder()
- .AddInMemoryCollection(values)
- .Build();
- filteredConfiguration.Bind(options);
+ section.Bind(options);
}
}
From fbf8c31470417752e15bd2e98f3ef40e73bf10f7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Aug 2026 13:20:49 +0000
Subject: [PATCH 37/43] =?UTF-8?q?CUDA=E3=83=84=E3=83=BC=E3=83=AB=E3=82=AD?=
=?UTF-8?q?=E3=83=83=E3=83=88=E3=82=B9=E3=83=86=E3=83=83=E3=83=97=E3=82=92?=
=?UTF-8?q?=E5=89=8A=E9=99=A4:=20PLamoPlugin=E3=81=AF=E5=90=8C=E6=A2=B1?=
=?UTF-8?q?=E5=AF=BE=E8=B1=A1=E5=A4=96=E3=81=AE=E3=81=9F=E3=82=81=E4=B8=8D?=
=?UTF-8?q?=E8=A6=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Freeesia <9002657+Freeesia@users.noreply.github.com>
---
.github/workflows/dotnet-desktop.yml | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml
index d78fa8d1..24eaf959 100644
--- a/.github/workflows/dotnet-desktop.yml
+++ b/.github/workflows/dotnet-desktop.yml
@@ -125,9 +125,6 @@ jobs:
versionSpec: "6.x"
- id: gitversion
uses: gittools/actions/gitversion/execute@v4.7.0
- - uses: Jimver/cuda-toolkit@v0.2.36
- with:
- cuda: '12.9.0'
- run: |
dotnet publish WindowTranslator -c Release -o publish --sc ${{ matrix.self }} `
-p:Version=${{ steps.gitversion.outputs.fullSemVer }} `
From ce46f8fb7d5d094cb7992445eb67d5c6f6608fca Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 19 Aug 2026 13:48:59 +0000
Subject: [PATCH 38/43] =?UTF-8?q?=E3=83=9E=E3=83=BC=E3=82=B8=E7=AB=B6?=
=?UTF-8?q?=E5=90=88=E3=81=AE=E8=AA=A4=E8=A7=A3=E6=B1=BA=E3=82=92=E4=BF=AE?=
=?UTF-8?q?=E6=AD=A3:=20CommonSettings.IsEnableAutoTarget=E3=82=92?=
=?UTF-8?q?=E5=89=8A=E9=99=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Freeesia <9002657+Freeesia@users.noreply.github.com>
---
WindowTranslator.Abstractions/UserSettings.cs | 5 -----
1 file changed, 5 deletions(-)
diff --git a/WindowTranslator.Abstractions/UserSettings.cs b/WindowTranslator.Abstractions/UserSettings.cs
index e043ead0..bf6f20db 100644
--- a/WindowTranslator.Abstractions/UserSettings.cs
+++ b/WindowTranslator.Abstractions/UserSettings.cs
@@ -41,11 +41,6 @@ public class CommonSettings
///
public bool IsOverlayPointSwap { get; set; }
- ///
- /// 自動的に翻訳を発動するか
- ///
- public bool IsEnableAutoTarget { get; set; }
-
///
/// プラグインストアの免責事項を非表示にするか
///
From 699641b402c2fd0451c3bccd40b5288a876a1df3 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Thu, 20 Aug 2026 23:21:44 +0900
Subject: [PATCH 39/43] =?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4?=
=?UTF-8?q?=E3=83=B3=E3=82=B9=E3=83=88=E3=82=A2=E3=81=AE=E3=82=A4=E3=83=B3?=
=?UTF-8?q?=E3=82=B9=E3=83=88=E3=83=BC=E3=83=AB=E5=BE=8C=E5=8B=95=E4=BD=9C?=
=?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
WindowTranslator.Abstractions/UserSettings.cs | 5 --
.../NuGetPluginServiceTests.cs | 56 +++++++++++-
WindowTranslator/ApplicationRestart.cs | 3 +-
.../PluginStore/NuGetPackageInstaller.cs | 13 ++-
.../Modules/PluginStore/NuGetPluginService.cs | 87 ++++++++++++++++---
.../PluginStore/PluginStoreViewModel.cs | 32 ++++++-
.../Modules/Settings/AllSettingsDialog.xaml | 5 ++
.../Modules/Settings/AllSettingsViewModel.cs | 2 -
.../Properties/Resources.Designer.cs | 5 ++
WindowTranslator/Properties/Resources.ar.resx | 3 +
WindowTranslator/Properties/Resources.cs.resx | 3 +
WindowTranslator/Properties/Resources.de.resx | 3 +
WindowTranslator/Properties/Resources.en.resx | 3 +
WindowTranslator/Properties/Resources.es.resx | 3 +
WindowTranslator/Properties/Resources.fa.resx | 3 +
.../Properties/Resources.fil.resx | 3 +
WindowTranslator/Properties/Resources.fr.resx | 3 +
WindowTranslator/Properties/Resources.hi.resx | 3 +
WindowTranslator/Properties/Resources.hu.resx | 3 +
WindowTranslator/Properties/Resources.id.resx | 3 +
WindowTranslator/Properties/Resources.ko.resx | 3 +
WindowTranslator/Properties/Resources.ms.resx | 3 +
WindowTranslator/Properties/Resources.pl.resx | 3 +
.../Properties/Resources.pt-BR.resx | 3 +
WindowTranslator/Properties/Resources.resx | 3 +
WindowTranslator/Properties/Resources.ru.resx | 3 +
WindowTranslator/Properties/Resources.th.resx | 3 +
WindowTranslator/Properties/Resources.tr.resx | 3 +
WindowTranslator/Properties/Resources.vi.resx | 3 +
.../Properties/Resources.zh-CN.resx | 3 +
.../Properties/Resources.zh-TW.resx | 3 +
31 files changed, 246 insertions(+), 28 deletions(-)
diff --git a/WindowTranslator.Abstractions/UserSettings.cs b/WindowTranslator.Abstractions/UserSettings.cs
index bf6f20db..ee018f1a 100644
--- a/WindowTranslator.Abstractions/UserSettings.cs
+++ b/WindowTranslator.Abstractions/UserSettings.cs
@@ -40,11 +40,6 @@ public class CommonSettings
/// オーバレイのポインター挙動を逆にするか
///
public bool IsOverlayPointSwap { get; set; }
-
- ///
- /// プラグインストアの免責事項を非表示にするか
- ///
- public bool HidePluginStoreDisclaimer { get; set; }
}
///
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index cf5afba4..d0cfe103 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -80,8 +80,9 @@ public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories
}));
using var service = CreateService(handler, testDirectory, hostMajorVersion: 7);
+ var progress = new RecordingProgress();
- await service.InstallPackageAsync("Root.Plugin", "1.0.0");
+ await service.InstallPackageAsync("Root.Plugin", "1.0.0", progress);
var pluginDirectory = Path.Combine(testDirectory, "Root.Plugin");
Assert.Equal("root", await File.ReadAllTextAsync(Path.Combine(pluginDirectory, "Root.Plugin.dll")));
@@ -120,6 +121,11 @@ await File.ReadAllTextAsync(Path.Combine(
Assert.Equal("1.0.0", package.Version);
Assert.Equal(7, package.HostMajorVersion);
Assert.True(package.IsCompatible);
+ Assert.Equal(0, progress.Values.First());
+ Assert.Equal(100, progress.Values.Last());
+ Assert.All(progress.Values, value => Assert.InRange(value, 0, 100));
+ Assert.True(progress.Values.SequenceEqual(progress.Values.OrderBy(value => value)));
+ Assert.Contains(progress.Values, value => value is > 0 and < 100);
}
finally
{
@@ -749,6 +755,47 @@ public async Task SearchReturnsReleaseAndPrereleaseVersions()
}
}
+ [Fact]
+ public async Task DisclaimerPreferenceIsStoredInThePluginManifestAndPreservedByPluginOperations()
+ {
+ var testDirectory = CreateTestDirectory();
+ try
+ {
+ using var handler = new InMemoryNuGetHandler();
+ handler.AddPackage(
+ "Root.Plugin",
+ "1.0.0",
+ CreatePackage(
+ "Root.Plugin",
+ "1.0.0",
+ [],
+ new Dictionary
+ {
+ ["lib/net10.0/Root.Plugin.dll"] = "root"u8.ToArray(),
+ }));
+
+ using (var service = CreateService(handler, testDirectory))
+ {
+ await service.SetHideDisclaimerAsync(true);
+ await service.InstallPackageAsync("Root.Plugin", "1.0.0");
+ await service.UninstallPackageAsync("Root.Plugin");
+ }
+
+ using (var service = CreateService(handler, testDirectory))
+ {
+ Assert.True(service.HideDisclaimer);
+ await service.SetHideDisclaimerAsync(false);
+ }
+
+ using var reloadedService = CreateService(handler, testDirectory);
+ Assert.False(reloadedService.HideDisclaimer);
+ }
+ finally
+ {
+ DeleteTestDirectory(testDirectory);
+ }
+ }
+
[Fact]
public async Task SearchUsesNuGetOwnersForOfficialPackageStatus()
{
@@ -1800,6 +1847,13 @@ private static NuGetPluginService CreateService(
hostPackageVersions ?? NuGetPluginService.CreateHostPackageVersions(),
hostMajorVersion ?? AppInfo.Instance.Version.Major);
+ private sealed class RecordingProgress : IProgress
+ {
+ public List Values { get; } = [];
+
+ public void Report(double value) => this.Values.Add(value);
+ }
+
private static TestPackageVersion CreatePluginVersionMetadata(
string version,
string? abstractionsRange = null,
diff --git a/WindowTranslator/ApplicationRestart.cs b/WindowTranslator/ApplicationRestart.cs
index 5024ad09..1dcaf38f 100644
--- a/WindowTranslator/ApplicationRestart.cs
+++ b/WindowTranslator/ApplicationRestart.cs
@@ -7,7 +7,6 @@ namespace WindowTranslator;
internal static class ApplicationRestart
{
internal const string RestartProcessIdArgument = "--windowtranslator-restart-pid";
- private static readonly TimeSpan PreviousProcessWaitTimeout = TimeSpan.FromSeconds(30);
public static void Restart()
{
@@ -38,7 +37,7 @@ public static string[] WaitForPreviousProcess(IEnumerable arguments)
try
{
using var process = Process.GetProcessById(processId.Value);
- _ = process.WaitForExit(PreviousProcessWaitTimeout);
+ process.WaitForExit();
}
catch (ArgumentException)
{
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
index 6379213b..3dc1a068 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
@@ -44,6 +44,7 @@ public async Task InstallAsync(
"WindowTranslatorPlugins",
Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDirectory);
+ progress?.Report(0);
try
{
@@ -53,15 +54,19 @@ public async Task InstallAsync(
workDirectory,
progress,
cancellationToken).ConfigureAwait(false);
+ progress?.Report(60);
Directory.CreateDirectory(destinationDirectory);
- foreach (var artifact in artifacts.OrderByDescending(a =>
- a.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)))
+ var orderedArtifacts = artifacts.OrderByDescending(a =>
+ a.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)).ToArray();
+ for (var index = 0; index < orderedArtifacts.Length; index++)
{
+ var artifact = orderedArtifacts[index];
ExtractPackageAssets(
artifact.PackagePath,
destinationDirectory,
requirePluginAssembly: artifact.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase));
+ progress?.Report(60 + (30d * (index + 1) / orderedArtifacts.Length));
}
var rootPackage = artifacts.First(artifact =>
@@ -310,7 +315,7 @@ private async Task DownloadPackageAsync(
CancellationToken cancellationToken)
{
this.logger.LogInformation("NuGetパッケージをダウンロード中: {PackageId} {Version}", packageId, version);
- progress?.Report(0);
+ progress?.Report(10);
await using var destination = File.Create(destinationPath);
var copied = await this.packageResource.CopyNupkgToStreamAsync(
packageId,
@@ -323,7 +328,7 @@ private async Task DownloadPackageAsync(
{
throw new InvalidOperationException($"NuGetパッケージを取得できませんでした: {packageId} {version}");
}
- progress?.Report(1);
+ progress?.Report(50);
}
private static PackageMetadata ReadPackageMetadata(string packagePath)
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
index ac0e1fb3..ac252097 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginService.cs
@@ -48,6 +48,8 @@ public sealed class NuGetPluginService : BackgroundService
private readonly AsyncSemaphore refreshLock = new(1);
private readonly object snapshotLock = new();
private PluginStoreSnapshot packageSnapshot = PluginStoreSnapshot.Empty;
+ private bool hideDisclaimer;
+ private int restartRequired;
private int disposed;
internal NuGetPluginService(
@@ -65,10 +67,15 @@ internal NuGetPluginService(
this.manifestPath = Path.Combine(this.nugetPluginsDir, "nuget-manifest.json");
this.hostPackageVersions = hostPackageVersions;
this.hostMajorVersion = hostMajorVersion;
+ this.hideDisclaimer = TryLoadHideDisclaimer(this.manifestPath, this.logger);
}
internal event EventHandler? PackageInformationUpdated;
+ internal bool HideDisclaimer => Volatile.Read(ref this.hideDisclaimer);
+
+ internal bool IsRestartRequired => Volatile.Read(ref this.restartRequired) != 0;
+
internal PluginStoreSnapshot PackageSnapshot
{
get
@@ -268,20 +275,27 @@ public async Task InstallPackageAsync(string packageId, string version, IProgres
Directory.Move(pluginOperation.TargetPath, pluginOperation.BackupPath);
}
Directory.Move(pluginOperation.WorkingPath, pluginOperation.TargetPath);
+ progress?.Report(95);
- var updatedManifest = new InstalledManifest(
- [
- .. currentManifest.Packages.Where(package =>
- !package.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)),
- new(
- packageId,
- version,
- this.hostMajorVersion,
- abstractionsVersionRange.ToString()),
- ]);
+ var updatedManifest = currentManifest with
+ {
+ Packages =
+ [
+ .. currentManifest.Packages.Where(package =>
+ !package.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase)),
+ new(
+ packageId,
+ version,
+ this.hostMajorVersion,
+ abstractionsVersionRange.ToString()),
+ ],
+ HideDisclaimer = this.HideDisclaimer,
+ };
await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
pluginOperation.Commit();
UpdateInstalledPackages(updatedManifest.Packages);
+ Volatile.Write(ref this.restartRequired, 1);
+ progress?.Report(100);
this.logger.LogInformation(
"パッケージのインストール完了: {PackageId} {Version} -> {TargetDir}",
@@ -299,16 +313,39 @@ public async Task UninstallPackageAsync(string packageId, CancellationToken canc
using var operation = await this.operationLock.EnterAsync(cancellationToken);
this.logger.LogInformation("パッケージをアンインストール: {PackageId}", packageId);
var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
- var updatedManifest = new InstalledManifest([.. manifest.Packages.Where(package =>
- !package.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase))]);
+ var updatedManifest = manifest with
+ {
+ Packages = [.. manifest.Packages.Where(package =>
+ !package.Id.Equals(packageId, StringComparison.OrdinalIgnoreCase))],
+ HideDisclaimer = this.HideDisclaimer,
+ };
await SaveManifestAsync(updatedManifest, cancellationToken).ConfigureAwait(false);
UpdateInstalledPackages(updatedManifest.Packages);
+ Volatile.Write(ref this.restartRequired, 1);
this.logger.LogInformation(
"パッケージ {PackageId} をアンインストール対象として記録しました。管理フォルダは次回起動時に削除されます。",
packageId);
}
+ internal async Task SetHideDisclaimerAsync(
+ bool value,
+ CancellationToken cancellationToken = default)
+ {
+ Volatile.Write(ref this.hideDisclaimer, value);
+ using var operation = await this.operationLock.EnterAsync(cancellationToken);
+ var manifest = await LoadManifestAsync(cancellationToken).ConfigureAwait(false);
+ var currentValue = this.HideDisclaimer;
+ if (manifest.HideDisclaimer == currentValue)
+ {
+ return;
+ }
+
+ await SaveManifestAsync(
+ manifest with { HideDisclaimer = currentValue },
+ cancellationToken).ConfigureAwait(false);
+ }
+
private async Task CreateCompatiblePackageInfoAsync(
IPackageSearchMetadata data,
PackageMetadataResource metadataResource,
@@ -480,6 +517,28 @@ private async Task LoadManifestAsync(CancellationToken cancel
private Task SaveManifestAsync(InstalledManifest manifest, CancellationToken cancellationToken)
=> SaveManifestAsync(this.manifestPath, manifest, cancellationToken);
+ private static bool TryLoadHideDisclaimer(
+ string manifestPath,
+ ILogger logger)
+ {
+ if (!File.Exists(manifestPath))
+ {
+ return false;
+ }
+
+ try
+ {
+ using var stream = File.OpenRead(manifestPath);
+ return JsonSerializer.Deserialize(stream, ManifestJsonOptions)
+ ?.HideDisclaimer ?? false;
+ }
+ catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
+ {
+ logger.LogWarning(ex, "プラグインマニフェストから免責事項の表示設定を読み込めませんでした。");
+ return false;
+ }
+ }
+
internal static async Task SaveManifestAsync(
string manifestPath,
InstalledManifest manifest,
@@ -541,7 +600,9 @@ public record InstalledPackageInfo(
}
/// NuGetプラグインの管理マニフェスト
-public record InstalledManifest(List Packages);
+public record InstalledManifest(
+ List Packages,
+ bool HideDisclaimer = false);
internal sealed record PluginStoreSnapshot(
IReadOnlyList InstalledPackages,
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index cc2e9d07..5c69b669 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -33,6 +33,9 @@ public partial class PluginStoreViewModel : ObservableObject, IDisposable
[ObservableProperty]
private bool hideDisclaimer;
+ [ObservableProperty]
+ private bool requiresRestart;
+
public bool HasError => this.ErrorMessage is not null;
public PluginPackageViewModel? SelectedPackage
@@ -74,10 +77,27 @@ public PluginStoreViewModel(
this.nugetService = nugetService;
this.logger = logger;
this.dialogService = dialogService;
+ this.hideDisclaimer = this.nugetService.HideDisclaimer;
+ this.requiresRestart = this.nugetService.IsRestartRequired;
this.nugetService.PackageInformationUpdated += OnPackageInformationUpdated;
ApplyPackageSnapshot(this.nugetService.PackageSnapshot);
}
+ partial void OnHideDisclaimerChanged(bool value)
+ => _ = SaveHideDisclaimerAsync(value);
+
+ private async Task SaveHideDisclaimerAsync(bool value)
+ {
+ try
+ {
+ await this.nugetService.SetHideDisclaimerAsync(value).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ this.logger.LogError(ex, "プラグインストアの免責事項の表示設定を保存できませんでした。");
+ }
+ }
+
private void OnPackageInformationUpdated(object? sender, EventArgs e)
{
if (this.disposed)
@@ -200,6 +220,7 @@ public async Task InstallAsync(
return;
}
+ package.InstallProgress = 0;
package.IsInstalling = true;
try
{
@@ -214,7 +235,7 @@ await this.nugetService.InstallPackageAsync(
package.IsInstalled = true;
package.InstalledVersion = version;
package.IsCompatible = true;
- package.InstallProgress = 0;
+ this.RequiresRestart = this.nugetService.IsRestartRequired;
this.logger.LogInformation("プラグインのインストール完了: {PackageId}", package.Id);
@@ -266,6 +287,7 @@ public async Task UninstallAsync(PluginPackageViewModel package)
package.IsInstalled = false;
package.InstalledVersion = null;
package.IsCompatible = true;
+ this.RequiresRestart = this.nugetService.IsRestartRequired;
await ShowRestartDialogAsync(Resources.Uninstall).ConfigureAwait(true);
}
@@ -297,10 +319,18 @@ private async Task ShowRestartDialogAsync(
}, cancellationToken).ConfigureAwait(true);
if (result == Wpf.Ui.Controls.ContentDialogResult.Primary)
{
+ await SaveHideDisclaimerAsync(this.HideDisclaimer).ConfigureAwait(true);
ApplicationRestart.Restart();
}
}
+ [RelayCommand]
+ private async Task RestartAsync()
+ {
+ await SaveHideDisclaimerAsync(this.HideDisclaimer).ConfigureAwait(true);
+ ApplicationRestart.Restart();
+ }
+
private void OnSelectedPackagePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (sender is PluginPackageViewModel package
diff --git a/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml b/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
index 8fdc62c8..26ecc0c0 100644
--- a/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
+++ b/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
@@ -204,6 +204,11 @@
+
t.Name, t => new TargetSettings()
{
diff --git a/WindowTranslator/Properties/Resources.Designer.cs b/WindowTranslator/Properties/Resources.Designer.cs
index 9412f83f..19c708e9 100644
--- a/WindowTranslator/Properties/Resources.Designer.cs
+++ b/WindowTranslator/Properties/Resources.Designer.cs
@@ -502,6 +502,11 @@ internal Resources() {
///
public static string RestartRequired => ResourceManager.GetString("RestartRequired", resourceCulture) ?? string.Empty;
+ ///
+ /// "プラグイン読み込み" に類似しているローカライズされた文字列を検索します。
+ ///
+ public static string LoadPlugins => ResourceManager.GetString("LoadPlugins", resourceCulture) ?? string.Empty;
+
///
/// "後で" に類似しているローカライズされた文字列を検索します。
///
diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx
index 3289c809..b3fcbf59 100644
--- a/WindowTranslator/Properties/Resources.ar.resx
+++ b/WindowTranslator/Properties/Resources.ar.resx
@@ -492,6 +492,9 @@
يرجى إعادة تشغيل WindowTranslator لتطبيق تغييرات المكون الإضافي.
+
+ تحميل المكونات الإضافية
+
صفحة المشروع
diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx
index a2b5c537..3dfebb41 100644
--- a/WindowTranslator/Properties/Resources.cs.resx
+++ b/WindowTranslator/Properties/Resources.cs.resx
@@ -382,6 +382,9 @@ Monitory nejsou podporovány.
Restartujte WindowTranslator, aby se změny pluginu projevily.
+
+ Načíst pluginy
+
Stránka projektu
diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx
index fc93ef57..e755ead8 100644
--- a/WindowTranslator/Properties/Resources.de.resx
+++ b/WindowTranslator/Properties/Resources.de.resx
@@ -501,6 +501,9 @@ Monitore werden nicht unterstützt.
Bitte starten Sie WindowTranslator neu, um Plugin-Änderungen anzuwenden.
+
+ Plugins laden
+
Projektseite
diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx
index 2e0b6e25..4155d462 100644
--- a/WindowTranslator/Properties/Resources.en.resx
+++ b/WindowTranslator/Properties/Resources.en.resx
@@ -501,6 +501,9 @@ Monitors are not supported.
Please restart WindowTranslator to apply plugin changes.
+
+ Load plugins
+
Project page
diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx
index ffc6dac3..a0049401 100644
--- a/WindowTranslator/Properties/Resources.es.resx
+++ b/WindowTranslator/Properties/Resources.es.resx
@@ -492,6 +492,9 @@
Reinicie WindowTranslator para aplicar los cambios del plugin.
+
+ Cargar plugins
+
Página del proyecto
diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx
index a3e2b349..ecae44cc 100644
--- a/WindowTranslator/Properties/Resources.fa.resx
+++ b/WindowTranslator/Properties/Resources.fa.resx
@@ -486,6 +486,9 @@
لطفاً WindowTranslator را مجدداً راهاندازی کنید تا تغییرات افزونه اعمال شود.
+
+ بارگذاری افزونهها
+
صفحه پروژه
diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx
index 5f30222c..c146ae50 100644
--- a/WindowTranslator/Properties/Resources.fil.resx
+++ b/WindowTranslator/Properties/Resources.fil.resx
@@ -501,6 +501,9 @@ Ang monitor ay hindi suportado.
Mangyaring i-restart ang WindowTranslator upang mailapat ang mga pagbabago sa plugin.
+
+ I-load ang mga plugin
+
Pahina ng proyekto
diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx
index eb9ec3a0..69cc698b 100644
--- a/WindowTranslator/Properties/Resources.fr.resx
+++ b/WindowTranslator/Properties/Resources.fr.resx
@@ -492,6 +492,9 @@
Veuillez redémarrer WindowTranslator pour appliquer les modifications de plugin.
+
+ Charger les plugins
+
Page du projet
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index d1b2c055..a5ac8e82 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -494,6 +494,9 @@
प्लगइन परिवर्तन लागू करने के लिए कृपया WindowTranslator को पुनः आरंभ करें।
+
+ प्लगइन लोड करें
+
प्रोजेक्ट पेज
diff --git a/WindowTranslator/Properties/Resources.hu.resx b/WindowTranslator/Properties/Resources.hu.resx
index 81daa280..f1976c1e 100644
--- a/WindowTranslator/Properties/Resources.hu.resx
+++ b/WindowTranslator/Properties/Resources.hu.resx
@@ -382,6 +382,9 @@ A monitorok nem támogatottak.
A bővítménymódosítások alkalmazásához indítsa újra a WindowTranslator alkalmazást.
+
+ Bővítmények betöltése
+
Projektoldal
diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx
index b156d6ff..081b2da6 100644
--- a/WindowTranslator/Properties/Resources.id.resx
+++ b/WindowTranslator/Properties/Resources.id.resx
@@ -500,6 +500,9 @@ Monitor tidak didukung.
Silakan restart WindowTranslator untuk menerapkan perubahan plugin.
+
+ Muat plugin
+
Halaman proyek
diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx
index a4fd69d5..e20a9a67 100644
--- a/WindowTranslator/Properties/Resources.ko.resx
+++ b/WindowTranslator/Properties/Resources.ko.resx
@@ -501,6 +501,9 @@
플러그인 변경 사항을 적용하려면 WindowTranslator를 다시 시작하세요.
+
+ 플러그인 불러오기
+
프로젝트 페이지
diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx
index 1c808bba..efcbf684 100644
--- a/WindowTranslator/Properties/Resources.ms.resx
+++ b/WindowTranslator/Properties/Resources.ms.resx
@@ -500,6 +500,9 @@ Monitor tidak disokong.
Sila mulakan semula WindowTranslator untuk menerapkan perubahan plugin.
+
+ Muatkan plugin
+
Halaman projek
diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx
index cf439427..f1702230 100644
--- a/WindowTranslator/Properties/Resources.pl.resx
+++ b/WindowTranslator/Properties/Resources.pl.resx
@@ -501,6 +501,9 @@ Monitory nie są obsługiwane.
Uruchom ponownie WindowTranslator, aby zastosować zmiany wtyczki.
+
+ Wczytaj wtyczki
+
Strona projektu
diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx
index c9b811c4..3b1cb83d 100644
--- a/WindowTranslator/Properties/Resources.pt-BR.resx
+++ b/WindowTranslator/Properties/Resources.pt-BR.resx
@@ -500,6 +500,9 @@ Monitor tidak didukung.
Reinicie o WindowTranslator para aplicar as alterações de plugin.
+
+ Carregar plugins
+
Página do projeto
diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx
index 6a2961c9..f5479496 100644
--- a/WindowTranslator/Properties/Resources.resx
+++ b/WindowTranslator/Properties/Resources.resx
@@ -501,6 +501,9 @@
プラグインの変更を適用するには、WindowTranslatorを再起動してください。
+
+ プラグイン読み込み
+
プロジェクトページ
diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx
index be912d52..5f40bad8 100644
--- a/WindowTranslator/Properties/Resources.ru.resx
+++ b/WindowTranslator/Properties/Resources.ru.resx
@@ -492,6 +492,9 @@
Перезапустите WindowTranslator для применения изменений плагина.
+
+ Загрузить плагины
+
Страница проекта
diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx
index df777cf0..6c58964b 100644
--- a/WindowTranslator/Properties/Resources.th.resx
+++ b/WindowTranslator/Properties/Resources.th.resx
@@ -501,6 +501,9 @@
กรุณาเริ่ม WindowTranslator ใหม่เพื่อนำการเปลี่ยนแปลงปลั๊กอินไปใช้
+
+ โหลดปลั๊กอิน
+
หน้าโครงการ
diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx
index 6979bcb0..81bb3ffe 100644
--- a/WindowTranslator/Properties/Resources.tr.resx
+++ b/WindowTranslator/Properties/Resources.tr.resx
@@ -501,6 +501,9 @@ Monitör desteklenmiyor.
Eklenti değişikliklerini uygulamak için lütfen WindowTranslator'ı yeniden başlatın.
+
+ Eklentileri yükle
+
Proje sayfası
diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx
index d5767ca9..3ed83e6f 100644
--- a/WindowTranslator/Properties/Resources.vi.resx
+++ b/WindowTranslator/Properties/Resources.vi.resx
@@ -501,6 +501,9 @@ Màn hình không được hỗ trợ.
Vui lòng khởi động lại WindowTranslator để áp dụng thay đổi plugin.
+
+ Tải plugin
+
Trang dự án
diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx
index c9fdc0b9..abc5d153 100644
--- a/WindowTranslator/Properties/Resources.zh-CN.resx
+++ b/WindowTranslator/Properties/Resources.zh-CN.resx
@@ -501,6 +501,9 @@
请重启 WindowTranslator 以应用插件更改。
+
+ 加载插件
+
项目页面
diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx
index 152d9978..63090dab 100644
--- a/WindowTranslator/Properties/Resources.zh-TW.resx
+++ b/WindowTranslator/Properties/Resources.zh-TW.resx
@@ -501,6 +501,9 @@
請重新啟動 WindowTranslator 以套用外掛程式變更。
+
+ 載入外掛程式
+
專案頁面
From c47cbb0a489f91a56b7a59533d7dfcc9a21bbd61 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sun, 23 Aug 2026 19:30:18 +0900
Subject: [PATCH 40/43] =?UTF-8?q?=E3=83=87=E3=83=90=E3=83=83=E3=82=B0?=
=?UTF-8?q?=E6=99=82=E3=81=AE=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4=E3=83=B3?=
=?UTF-8?q?=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=81=BF=E3=81=A8=E8=A1=A8=E7=A4=BA?=
=?UTF-8?q?=E6=96=87=E8=A8=80=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Modules/PluginStore/NuGetPluginCatalog.cs | 14 ++++++++++++++
.../Modules/PluginStore/PluginStoreViewModel.cs | 2 +-
.../Modules/Settings/AllSettingsDialog.xaml | 2 +-
WindowTranslator/Properties/Resources.Designer.cs | 4 ++--
WindowTranslator/Properties/Resources.ar.resx | 4 ++--
WindowTranslator/Properties/Resources.cs.resx | 4 ++--
WindowTranslator/Properties/Resources.de.resx | 4 ++--
WindowTranslator/Properties/Resources.en.resx | 4 ++--
WindowTranslator/Properties/Resources.es.resx | 4 ++--
WindowTranslator/Properties/Resources.fa.resx | 4 ++--
WindowTranslator/Properties/Resources.fil.resx | 4 ++--
WindowTranslator/Properties/Resources.fr.resx | 4 ++--
WindowTranslator/Properties/Resources.hi.resx | 4 ++--
WindowTranslator/Properties/Resources.hu.resx | 4 ++--
WindowTranslator/Properties/Resources.id.resx | 4 ++--
WindowTranslator/Properties/Resources.ko.resx | 4 ++--
WindowTranslator/Properties/Resources.ms.resx | 4 ++--
WindowTranslator/Properties/Resources.pl.resx | 4 ++--
WindowTranslator/Properties/Resources.pt-BR.resx | 4 ++--
WindowTranslator/Properties/Resources.resx | 4 ++--
WindowTranslator/Properties/Resources.ru.resx | 4 ++--
WindowTranslator/Properties/Resources.th.resx | 4 ++--
WindowTranslator/Properties/Resources.tr.resx | 4 ++--
WindowTranslator/Properties/Resources.vi.resx | 4 ++--
WindowTranslator/Properties/Resources.zh-CN.resx | 4 ++--
WindowTranslator/Properties/Resources.zh-TW.resx | 4 ++--
26 files changed, 62 insertions(+), 48 deletions(-)
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
index 240241dc..ef31aa1e 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPluginCatalog.cs
@@ -19,6 +19,20 @@ public sealed class NuGetPluginCatalog : IPluginCatalog
private static readonly string DefaultTempDir =
Path.Combine(Path.GetTempPath(), "WindowTranslator", "nuget-plugins");
+ static NuGetPluginCatalog()
+ {
+#if DISABLE_PLUGIN_COMPATIBILITY_VALIDATION
+ var hostAbstractions = typeof(UserSettings).Assembly;
+ AssemblyLoadContext.Default.Resolving += (_, assemblyName) =>
+ string.Equals(
+ assemblyName.Name,
+ hostAbstractions.GetName().Name,
+ StringComparison.OrdinalIgnoreCase)
+ ? hostAbstractions
+ : null;
+#endif
+ }
+
private readonly string sourceDir;
private readonly string tempDir;
private readonly int hostMajorVersion;
diff --git a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
index 5c69b669..50513d89 100644
--- a/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
+++ b/WindowTranslator/Modules/PluginStore/PluginStoreViewModel.cs
@@ -315,7 +315,7 @@ private async Task ShowRestartDialogAsync(
Title = title,
Content = Resources.RestartRequired,
PrimaryButtonText = Resources.RestartNow,
- CloseButtonText = Resources.Close,
+ CloseButtonText = Resources.ReviewLater,
}, cancellationToken).ConfigureAwait(true);
if (result == Wpf.Ui.Controls.ContentDialogResult.Primary)
{
diff --git a/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml b/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
index 26ecc0c0..70a9b153 100644
--- a/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
+++ b/WindowTranslator/Modules/Settings/AllSettingsDialog.xaml
@@ -206,7 +206,7 @@
ResourceManager.GetString("RestartRequired", resourceCulture) ?? string.Empty;
///
- /// "プラグイン読み込み" に類似しているローカライズされた文字列を検索します。
+ /// "再起動" に類似しているローカライズされた文字列を検索します。
///
- public static string LoadPlugins => ResourceManager.GetString("LoadPlugins", resourceCulture) ?? string.Empty;
+ public static string Restart => ResourceManager.GetString("Restart", resourceCulture) ?? string.Empty;
///
/// "後で" に類似しているローカライズされた文字列を検索します。
diff --git a/WindowTranslator/Properties/Resources.ar.resx b/WindowTranslator/Properties/Resources.ar.resx
index b3fcbf59..b042929a 100644
--- a/WindowTranslator/Properties/Resources.ar.resx
+++ b/WindowTranslator/Properties/Resources.ar.resx
@@ -492,8 +492,8 @@
يرجى إعادة تشغيل WindowTranslator لتطبيق تغييرات المكون الإضافي.
-
- تحميل المكونات الإضافية
+
+ إعادة التشغيل
صفحة المشروع
diff --git a/WindowTranslator/Properties/Resources.cs.resx b/WindowTranslator/Properties/Resources.cs.resx
index 3dfebb41..9886e4b4 100644
--- a/WindowTranslator/Properties/Resources.cs.resx
+++ b/WindowTranslator/Properties/Resources.cs.resx
@@ -382,8 +382,8 @@ Monitory nejsou podporovány.
Restartujte WindowTranslator, aby se změny pluginu projevily.
-
- Načíst pluginy
+
+ Restartovat
Stránka projektu
diff --git a/WindowTranslator/Properties/Resources.de.resx b/WindowTranslator/Properties/Resources.de.resx
index e755ead8..6e270c9c 100644
--- a/WindowTranslator/Properties/Resources.de.resx
+++ b/WindowTranslator/Properties/Resources.de.resx
@@ -501,8 +501,8 @@ Monitore werden nicht unterstützt.
Bitte starten Sie WindowTranslator neu, um Plugin-Änderungen anzuwenden.
-
- Plugins laden
+
+ Neu starten
Projektseite
diff --git a/WindowTranslator/Properties/Resources.en.resx b/WindowTranslator/Properties/Resources.en.resx
index 4155d462..d3e76d5e 100644
--- a/WindowTranslator/Properties/Resources.en.resx
+++ b/WindowTranslator/Properties/Resources.en.resx
@@ -501,8 +501,8 @@ Monitors are not supported.
Please restart WindowTranslator to apply plugin changes.
-
- Load plugins
+
+ Restart
Project page
diff --git a/WindowTranslator/Properties/Resources.es.resx b/WindowTranslator/Properties/Resources.es.resx
index a0049401..b85f99a1 100644
--- a/WindowTranslator/Properties/Resources.es.resx
+++ b/WindowTranslator/Properties/Resources.es.resx
@@ -492,8 +492,8 @@
Reinicie WindowTranslator para aplicar los cambios del plugin.
-
- Cargar plugins
+
+ Reiniciar
Página del proyecto
diff --git a/WindowTranslator/Properties/Resources.fa.resx b/WindowTranslator/Properties/Resources.fa.resx
index ecae44cc..13f059d9 100644
--- a/WindowTranslator/Properties/Resources.fa.resx
+++ b/WindowTranslator/Properties/Resources.fa.resx
@@ -486,8 +486,8 @@
لطفاً WindowTranslator را مجدداً راهاندازی کنید تا تغییرات افزونه اعمال شود.
-
- بارگذاری افزونهها
+
+ راهاندازی مجدد
صفحه پروژه
diff --git a/WindowTranslator/Properties/Resources.fil.resx b/WindowTranslator/Properties/Resources.fil.resx
index c146ae50..8abb2184 100644
--- a/WindowTranslator/Properties/Resources.fil.resx
+++ b/WindowTranslator/Properties/Resources.fil.resx
@@ -501,8 +501,8 @@ Ang monitor ay hindi suportado.
Mangyaring i-restart ang WindowTranslator upang mailapat ang mga pagbabago sa plugin.
-
- I-load ang mga plugin
+
+ I-restart
Pahina ng proyekto
diff --git a/WindowTranslator/Properties/Resources.fr.resx b/WindowTranslator/Properties/Resources.fr.resx
index 69cc698b..d6b28f98 100644
--- a/WindowTranslator/Properties/Resources.fr.resx
+++ b/WindowTranslator/Properties/Resources.fr.resx
@@ -492,8 +492,8 @@
Veuillez redémarrer WindowTranslator pour appliquer les modifications de plugin.
-
- Charger les plugins
+
+ Redémarrer
Page du projet
diff --git a/WindowTranslator/Properties/Resources.hi.resx b/WindowTranslator/Properties/Resources.hi.resx
index a5ac8e82..c69e2f92 100644
--- a/WindowTranslator/Properties/Resources.hi.resx
+++ b/WindowTranslator/Properties/Resources.hi.resx
@@ -494,8 +494,8 @@
प्लगइन परिवर्तन लागू करने के लिए कृपया WindowTranslator को पुनः आरंभ करें।
-
- प्लगइन लोड करें
+
+ पुनः आरंभ करें
प्रोजेक्ट पेज
diff --git a/WindowTranslator/Properties/Resources.hu.resx b/WindowTranslator/Properties/Resources.hu.resx
index f1976c1e..f8d7d132 100644
--- a/WindowTranslator/Properties/Resources.hu.resx
+++ b/WindowTranslator/Properties/Resources.hu.resx
@@ -382,8 +382,8 @@ A monitorok nem támogatottak.
A bővítménymódosítások alkalmazásához indítsa újra a WindowTranslator alkalmazást.
-
- Bővítmények betöltése
+
+ Újraindítás
Projektoldal
diff --git a/WindowTranslator/Properties/Resources.id.resx b/WindowTranslator/Properties/Resources.id.resx
index 081b2da6..8189ca16 100644
--- a/WindowTranslator/Properties/Resources.id.resx
+++ b/WindowTranslator/Properties/Resources.id.resx
@@ -500,8 +500,8 @@ Monitor tidak didukung.
Silakan restart WindowTranslator untuk menerapkan perubahan plugin.
-
- Muat plugin
+
+ Mulai ulang
Halaman proyek
diff --git a/WindowTranslator/Properties/Resources.ko.resx b/WindowTranslator/Properties/Resources.ko.resx
index e20a9a67..6bc74f4f 100644
--- a/WindowTranslator/Properties/Resources.ko.resx
+++ b/WindowTranslator/Properties/Resources.ko.resx
@@ -501,8 +501,8 @@
플러그인 변경 사항을 적용하려면 WindowTranslator를 다시 시작하세요.
-
- 플러그인 불러오기
+
+ 다시 시작
프로젝트 페이지
diff --git a/WindowTranslator/Properties/Resources.ms.resx b/WindowTranslator/Properties/Resources.ms.resx
index efcbf684..5bd75f0b 100644
--- a/WindowTranslator/Properties/Resources.ms.resx
+++ b/WindowTranslator/Properties/Resources.ms.resx
@@ -500,8 +500,8 @@ Monitor tidak disokong.
Sila mulakan semula WindowTranslator untuk menerapkan perubahan plugin.
-
- Muatkan plugin
+
+ Mulakan semula
Halaman projek
diff --git a/WindowTranslator/Properties/Resources.pl.resx b/WindowTranslator/Properties/Resources.pl.resx
index f1702230..aa01d8e1 100644
--- a/WindowTranslator/Properties/Resources.pl.resx
+++ b/WindowTranslator/Properties/Resources.pl.resx
@@ -501,8 +501,8 @@ Monitory nie są obsługiwane.
Uruchom ponownie WindowTranslator, aby zastosować zmiany wtyczki.
-
- Wczytaj wtyczki
+
+ Uruchom ponownie
Strona projektu
diff --git a/WindowTranslator/Properties/Resources.pt-BR.resx b/WindowTranslator/Properties/Resources.pt-BR.resx
index 3b1cb83d..a97ba41c 100644
--- a/WindowTranslator/Properties/Resources.pt-BR.resx
+++ b/WindowTranslator/Properties/Resources.pt-BR.resx
@@ -500,8 +500,8 @@ Monitor tidak didukung.
Reinicie o WindowTranslator para aplicar as alterações de plugin.
-
- Carregar plugins
+
+ Reiniciar
Página do projeto
diff --git a/WindowTranslator/Properties/Resources.resx b/WindowTranslator/Properties/Resources.resx
index f5479496..7d3e346f 100644
--- a/WindowTranslator/Properties/Resources.resx
+++ b/WindowTranslator/Properties/Resources.resx
@@ -501,8 +501,8 @@
プラグインの変更を適用するには、WindowTranslatorを再起動してください。
-
- プラグイン読み込み
+
+ 再起動
プロジェクトページ
diff --git a/WindowTranslator/Properties/Resources.ru.resx b/WindowTranslator/Properties/Resources.ru.resx
index 5f40bad8..01b5b10c 100644
--- a/WindowTranslator/Properties/Resources.ru.resx
+++ b/WindowTranslator/Properties/Resources.ru.resx
@@ -492,8 +492,8 @@
Перезапустите WindowTranslator для применения изменений плагина.
-
- Загрузить плагины
+
+ Перезапустить
Страница проекта
diff --git a/WindowTranslator/Properties/Resources.th.resx b/WindowTranslator/Properties/Resources.th.resx
index 6c58964b..09655236 100644
--- a/WindowTranslator/Properties/Resources.th.resx
+++ b/WindowTranslator/Properties/Resources.th.resx
@@ -501,8 +501,8 @@
กรุณาเริ่ม WindowTranslator ใหม่เพื่อนำการเปลี่ยนแปลงปลั๊กอินไปใช้
-
- โหลดปลั๊กอิน
+
+ เริ่มใหม่
หน้าโครงการ
diff --git a/WindowTranslator/Properties/Resources.tr.resx b/WindowTranslator/Properties/Resources.tr.resx
index 81bb3ffe..68f78557 100644
--- a/WindowTranslator/Properties/Resources.tr.resx
+++ b/WindowTranslator/Properties/Resources.tr.resx
@@ -501,8 +501,8 @@ Monitör desteklenmiyor.
Eklenti değişikliklerini uygulamak için lütfen WindowTranslator'ı yeniden başlatın.
-
- Eklentileri yükle
+
+ Yeniden başlat
Proje sayfası
diff --git a/WindowTranslator/Properties/Resources.vi.resx b/WindowTranslator/Properties/Resources.vi.resx
index 3ed83e6f..773f02c0 100644
--- a/WindowTranslator/Properties/Resources.vi.resx
+++ b/WindowTranslator/Properties/Resources.vi.resx
@@ -501,8 +501,8 @@ Màn hình không được hỗ trợ.
Vui lòng khởi động lại WindowTranslator để áp dụng thay đổi plugin.
-
- Tải plugin
+
+ Khởi động lại
Trang dự án
diff --git a/WindowTranslator/Properties/Resources.zh-CN.resx b/WindowTranslator/Properties/Resources.zh-CN.resx
index abc5d153..f2d61645 100644
--- a/WindowTranslator/Properties/Resources.zh-CN.resx
+++ b/WindowTranslator/Properties/Resources.zh-CN.resx
@@ -501,8 +501,8 @@
请重启 WindowTranslator 以应用插件更改。
-
- 加载插件
+
+ 重启
项目页面
diff --git a/WindowTranslator/Properties/Resources.zh-TW.resx b/WindowTranslator/Properties/Resources.zh-TW.resx
index 63090dab..cf7700ec 100644
--- a/WindowTranslator/Properties/Resources.zh-TW.resx
+++ b/WindowTranslator/Properties/Resources.zh-TW.resx
@@ -501,8 +501,8 @@
請重新啟動 WindowTranslator 以套用外掛程式變更。
-
- 載入外掛程式
+
+ 重新啟動
專案頁面
From 6a88a8811cbfbc2112ca4f0e9dac1320f356b28a Mon Sep 17 00:00:00 2001
From: Freesia
Date: Sun, 23 Aug 2026 23:11:03 +0900
Subject: [PATCH 41/43] =?UTF-8?q?NuGet=E3=83=A9=E3=83=B3=E3=82=BF=E3=82=A4?=
=?UTF-8?q?=E3=83=A0=E8=B3=87=E7=94=A3=E3=81=AE=E5=B1=95=E9=96=8B=E6=96=B9?=
=?UTF-8?q?=E6=B3=95=E3=82=92=E4=BF=AE=E6=AD=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../NuGetPluginServiceTests.cs | 29 +++++++++++++------
.../PluginStore/NuGetPackageInstaller.cs | 17 ++++++-----
2 files changed, 29 insertions(+), 17 deletions(-)
diff --git a/WindowTranslator.Tests/NuGetPluginServiceTests.cs b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
index d0cfe103..c6a0374d 100644
--- a/WindowTranslator.Tests/NuGetPluginServiceTests.cs
+++ b/WindowTranslator.Tests/NuGetPluginServiceTests.cs
@@ -33,7 +33,7 @@ public sealed class NuGetPluginServiceTests
private static readonly string RuntimeIdentifier = RuntimeInformation.RuntimeIdentifier;
[Fact]
- public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories()
+ public async Task InstallResolvesDependenciesUsingNuGetRuntimeAssetLayout()
{
var testDirectory = CreateTestDirectory();
try
@@ -53,7 +53,7 @@ public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories
{
["lib/net10.0/Root.Plugin.dll"] = "root"u8.ToArray(),
["lib/net10.0/fr/Root.Plugin.resources.dll"] = "fr"u8.ToArray(),
- [$"runtimes/{RuntimeIdentifier}/native/root-native.dll"] = "native"u8.ToArray(),
+ [$"runtimes/{RuntimeIdentifier}/native/subdirectory/root-native.dll"] = "native"u8.ToArray(),
[$"lib/net10.0/runtimes/{RuntimeIdentifier}/native/custom-native.dll"] = "custom"u8.ToArray(),
}));
handler.AddPackage(
@@ -65,7 +65,9 @@ public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories
[new("Transitive.Package", "[2.0.0]")],
new Dictionary
{
- ["lib/net8.0/Dependency.Package.dll"] = "dependency"u8.ToArray(),
+ ["lib/net8.0/Dependency.Package.dll"] = "fallback"u8.ToArray(),
+ [$"runtimes/{RuntimeIdentifier}/lib/net8.0/Dependency.Package.dll"] =
+ "dependency"u8.ToArray(),
}));
handler.AddPackage(
"Transitive.Package",
@@ -92,17 +94,26 @@ public async Task InstallResolvesRuntimeDependenciesAndPreservesAssetDirectories
Assert.Equal(
"dependency",
await File.ReadAllTextAsync(Path.Combine(pluginDirectory, "Dependency.Package.dll")));
+ Assert.False(File.Exists(Path.Combine(
+ pluginDirectory,
+ "runtimes",
+ RuntimeIdentifier,
+ "lib",
+ "net8.0",
+ "Dependency.Package.dll")));
Assert.Equal(
"transitive",
await File.ReadAllTextAsync(Path.Combine(pluginDirectory, "Transitive.Package.dll")));
Assert.Equal(
"native",
- await File.ReadAllTextAsync(Path.Combine(
- pluginDirectory,
- "runtimes",
- RuntimeIdentifier,
- "native",
- "root-native.dll")));
+ await File.ReadAllTextAsync(Path.Combine(pluginDirectory, "root-native.dll")));
+ Assert.False(File.Exists(Path.Combine(
+ pluginDirectory,
+ "runtimes",
+ RuntimeIdentifier,
+ "native",
+ "subdirectory",
+ "root-native.dll")));
Assert.Equal(
"custom",
await File.ReadAllTextAsync(Path.Combine(
diff --git a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
index 3dc1a068..6d83d90b 100644
--- a/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
+++ b/WindowTranslator/Modules/PluginStore/NuGetPackageInstaller.cs
@@ -436,9 +436,9 @@ private static void ExtractPackageAssets(
.Where(e => e.FullName.Split('/').Length >= 3)
.GroupBy(e => e.FullName.Split('/')[1])
.ToArray();
- var hasPluginAssembly = false;
+ var hasPluginAssembly = ExtractRuntimeAssets(archive, destinationDirectory);
- if (libGroups.Length > 0)
+ if (!hasPluginAssembly && libGroups.Length > 0)
{
var selectedFramework = SelectBestTfm(libGroups.Select(g => g.Key));
if (selectedFramework is not null)
@@ -456,7 +456,6 @@ private static void ExtractPackageAssets(
}
}
- hasPluginAssembly |= ExtractRuntimeAssets(archive, destinationDirectory);
if (requirePluginAssembly && !hasPluginAssembly)
{
throw new InvalidOperationException("プラグインパッケージに互換性のあるアセンブリが見つかりませんでした。");
@@ -490,7 +489,7 @@ private static bool ExtractRuntimeAssets(ZipArchive archive, string destinationD
e.Name.EndsWith(".dll", StringComparison.OrdinalIgnoreCase));
ExtractEntries(
selectedEntries,
- string.Empty,
+ $"{runtimeLibPrefix}{selectedFramework}/",
destinationDirectory);
}
}
@@ -508,19 +507,21 @@ private static bool ExtractRuntimeAssets(ZipArchive archive, string destinationD
ExtractEntries(
archive.Entries.Where(e => e.FullName.StartsWith(nativePrefix, StringComparison.OrdinalIgnoreCase)
&& !string.IsNullOrEmpty(e.Name)),
- string.Empty,
- destinationDirectory);
+ nativePrefix,
+ destinationDirectory,
+ flatten: true);
return hasManagedAssembly;
}
private static void ExtractEntries(
IEnumerable entries,
string prefix,
- string destinationDirectory)
+ string destinationDirectory,
+ bool flatten = false)
{
foreach (var entry in entries)
{
- var relativePath = entry.FullName[prefix.Length..];
+ var relativePath = flatten ? entry.Name : entry.FullName[prefix.Length..];
if (string.IsNullOrWhiteSpace(relativePath))
{
continue;
From e6039cfeab2ef3815519c2241fad7fb7664b0df5 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Mon, 24 Aug 2026 00:54:43 +0900
Subject: [PATCH 42/43] =?UTF-8?q?GitHub=20Copilot=E3=81=AECLI=E3=82=92?=
=?UTF-8?q?=E3=83=97=E3=83=A9=E3=82=B0=E3=82=A4=E3=83=B3=E3=81=B8=E5=90=8C?=
=?UTF-8?q?=E6=A2=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../WindowTranslator.Plugin.GitHubCopilotPlugin.csproj | 1 +
1 file changed, 1 insertion(+)
diff --git a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
index db0736dd..c27723be 100644
--- a/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.GitHubCopilotPlugin/WindowTranslator.Plugin.GitHubCopilotPlugin.csproj
@@ -5,6 +5,7 @@
Translation for WindowTranslator using GitHub Copilot.
true
false
+ true
From cc9f96f72f89b21beb004a2983f45017f49a5751 Mon Sep 17 00:00:00 2001
From: Freesia
Date: Thu, 27 Aug 2026 01:18:57 +0900
Subject: [PATCH 43/43] =?UTF-8?q?TesseractOCR=E3=81=AE=E3=83=8D=E3=82=A4?=
=?UTF-8?q?=E3=83=86=E3=82=A3=E3=83=96DLL=E3=82=92=E5=90=8C=E6=A2=B1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Plugins/Directory.Build.targets | 6 ++++++
.../WindowTranslator.Plugin.TesseractOCRPlugin.csproj | 1 +
2 files changed, 7 insertions(+)
diff --git a/Plugins/Directory.Build.targets b/Plugins/Directory.Build.targets
index 3244b5a0..6a5a07fd 100644
--- a/Plugins/Directory.Build.targets
+++ b/Plugins/Directory.Build.targets
@@ -19,12 +19,18 @@
<_PluginWinX64RuntimeAsset Include="$(TargetDir)runtimes\win-x64\**\*" />
<_PluginWinRuntimeAsset Include="$(TargetDir)runtimes\win\**\*" />
<_PluginAnyRuntimeAsset Include="$(TargetDir)runtimes\any\**\*" />
+ <_PluginX64RuntimeAsset Include="$(TargetDir)x64\**\*" />
+ <_PluginX86RuntimeAsset Include="$(TargetDir)x86\**\*" />
+
+
diff --git a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
index 89d62cfd..2cc64f80 100644
--- a/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
+++ b/Plugins/WindowTranslator.Plugin.TesseractOCRPlugin/WindowTranslator.Plugin.TesseractOCRPlugin.csproj
@@ -6,6 +6,7 @@
OCR for WindowTranslator using the Tesseract engine.
true
false
+ true