Settings: Database section with connection params, test, and replay button
New « BASE DE DONNÉES (XAMPP / MySQL) » section in Paramètres covering: - Host / Port / User / Password / Database — defaults match a fresh XAMPP install (localhost:3306, root, empty password, "proserve"). Password is DPAPI-encrypted in config.json (same scheme as the license key). - « Tester » button — opens a fresh MySqlConnection and runs SELECT 1. Shows ✓ OK or the connection error inline. - « Appliquer automatiquement les migrations à l'install » checkbox — opt-in for the post-install migration step. Default true. - « 🔁 Rejouer les migrations » button — manually re-runs ApplyMigrationsAsync on the latest installed version. Useful when the post-install run failed (XAMPP was off) or after a dev added a new SQL file. Live status « 3/5 : 0042_add_index.sql » + final « ✓ N applied, M skipped » or « ✗ failure ». - Hint paragraph below explaining the _migrations/ convention. SettingsViewModel - Pulls IDatabaseMigrationService and IInstallationRegistry via DI. - Save() now also persists the DB block, encrypting the password before write. - TestDbAsync temporarily swaps the in-memory config so the service sees the values being typed (without persisting until Save). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,8 +8,10 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using PSLauncher.Core.Configuration;
|
using PSLauncher.Core.Configuration;
|
||||||
using PSLauncher.Core.Downloads;
|
using PSLauncher.Core.Downloads;
|
||||||
|
using PSLauncher.Core.Installations;
|
||||||
using PSLauncher.Core.Licensing;
|
using PSLauncher.Core.Licensing;
|
||||||
using PSLauncher.Core.Localization;
|
using PSLauncher.Core.Localization;
|
||||||
|
using PSLauncher.Core.Migrations;
|
||||||
using PSLauncher.Models;
|
using PSLauncher.Models;
|
||||||
|
|
||||||
namespace PSLauncher.App.ViewModels;
|
namespace PSLauncher.App.ViewModels;
|
||||||
@@ -27,6 +29,8 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
private readonly LocalConfig _config;
|
private readonly LocalConfig _config;
|
||||||
private readonly ILicenseService _licenseService;
|
private readonly ILicenseService _licenseService;
|
||||||
private readonly IDownloadStateStore _downloadStore;
|
private readonly IDownloadStateStore _downloadStore;
|
||||||
|
private readonly IDatabaseMigrationService _migrationService;
|
||||||
|
private readonly IInstallationRegistry _registry;
|
||||||
private readonly HttpClient _http;
|
private readonly HttpClient _http;
|
||||||
private readonly ILogger<SettingsViewModel> _logger;
|
private readonly ILogger<SettingsViewModel> _logger;
|
||||||
|
|
||||||
@@ -39,6 +43,18 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
[ObservableProperty] private string _installRoot;
|
[ObservableProperty] private string _installRoot;
|
||||||
[ObservableProperty] private string _language;
|
[ObservableProperty] private string _language;
|
||||||
|
|
||||||
|
// ----- DB Migration settings -----
|
||||||
|
[ObservableProperty] private string _dbHost;
|
||||||
|
[ObservableProperty] private uint _dbPort;
|
||||||
|
[ObservableProperty] private string _dbUser;
|
||||||
|
[ObservableProperty] private string _dbPassword;
|
||||||
|
[ObservableProperty] private string _dbName;
|
||||||
|
[ObservableProperty] private bool _dbAutoApplyMigrations;
|
||||||
|
[ObservableProperty] private string? _dbConnectionStatus;
|
||||||
|
[ObservableProperty] private bool _isTestingDb;
|
||||||
|
[ObservableProperty] private string? _migrationsReplayStatus;
|
||||||
|
[ObservableProperty] private bool _isReplayingMigrations;
|
||||||
|
|
||||||
public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
|
public IReadOnlyList<LanguageOption> AvailableLanguages { get; } =
|
||||||
PSLauncher.Core.Localization.Strings.Available
|
PSLauncher.Core.Localization.Strings.Available
|
||||||
.Select(t => new LanguageOption(t.Code, t.Name))
|
.Select(t => new LanguageOption(t.Code, t.Name))
|
||||||
@@ -105,6 +121,8 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
LocalConfig config,
|
LocalConfig config,
|
||||||
ILicenseService licenseService,
|
ILicenseService licenseService,
|
||||||
IDownloadStateStore downloadStore,
|
IDownloadStateStore downloadStore,
|
||||||
|
IDatabaseMigrationService migrationService,
|
||||||
|
IInstallationRegistry registry,
|
||||||
HttpClient http,
|
HttpClient http,
|
||||||
ILogger<SettingsViewModel> logger)
|
ILogger<SettingsViewModel> logger)
|
||||||
{
|
{
|
||||||
@@ -112,6 +130,8 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
_config = config;
|
_config = config;
|
||||||
_licenseService = licenseService;
|
_licenseService = licenseService;
|
||||||
_downloadStore = downloadStore;
|
_downloadStore = downloadStore;
|
||||||
|
_migrationService = migrationService;
|
||||||
|
_registry = registry;
|
||||||
_http = http;
|
_http = http;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
@@ -119,6 +139,13 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
_installRoot = config.InstallRoot;
|
_installRoot = config.InstallRoot;
|
||||||
_language = string.IsNullOrWhiteSpace(config.Language) ? "auto" : config.Language;
|
_language = string.IsNullOrWhiteSpace(config.Language) ? "auto" : config.Language;
|
||||||
|
|
||||||
|
_dbHost = config.Database.Host;
|
||||||
|
_dbPort = config.Database.Port;
|
||||||
|
_dbUser = config.Database.User;
|
||||||
|
_dbPassword = DatabasePasswordProtector.Unprotect(config.Database.EncryptedPassword) ?? string.Empty;
|
||||||
|
_dbName = config.Database.Database;
|
||||||
|
_dbAutoApplyMigrations = config.Database.AutoApplyMigrations;
|
||||||
|
|
||||||
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
LauncherVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0";
|
||||||
MachineId = licenseService.GetMachineId();
|
MachineId = licenseService.GetMachineId();
|
||||||
LogsDirectory = App.LogsDirectory;
|
LogsDirectory = App.LogsDirectory;
|
||||||
@@ -225,6 +252,17 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
_config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/');
|
_config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/');
|
||||||
_config.InstallRoot = InstallRoot;
|
_config.InstallRoot = InstallRoot;
|
||||||
_config.Language = Language ?? "auto";
|
_config.Language = Language ?? "auto";
|
||||||
|
|
||||||
|
// DB settings
|
||||||
|
_config.Database.Host = DbHost.Trim();
|
||||||
|
_config.Database.Port = DbPort > 0 ? DbPort : 3306;
|
||||||
|
_config.Database.User = DbUser.Trim();
|
||||||
|
_config.Database.EncryptedPassword = string.IsNullOrEmpty(DbPassword)
|
||||||
|
? null
|
||||||
|
: DatabasePasswordProtector.Protect(DbPassword);
|
||||||
|
_config.Database.Database = DbName.Trim();
|
||||||
|
_config.Database.AutoApplyMigrations = DbAutoApplyMigrations;
|
||||||
|
|
||||||
_configStore.Save(_config);
|
_configStore.Save(_config);
|
||||||
_logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged);
|
_logger.LogInformation("Settings saved (language={Lang}, changed={Changed})", _config.Language, languageChanged);
|
||||||
|
|
||||||
@@ -289,4 +327,99 @@ public sealed partial class SettingsViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatSize(long bytes) => Strings.FormatSize(bytes);
|
private static string FormatSize(long bytes) => Strings.FormatSize(bytes);
|
||||||
|
|
||||||
|
// ===================== DB MIGRATIONS =====================
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test la connexion à la DB MySQL avec les paramètres en cours de saisie.
|
||||||
|
/// Persiste temporairement les valeurs sur _config.Database avant le test
|
||||||
|
/// pour que le service les voie via son configProvider, puis remet (et le
|
||||||
|
/// Save() final écrasera quand l'utilisateur cliquera Enregistrer).
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task TestDbAsync()
|
||||||
|
{
|
||||||
|
DbConnectionStatus = Strings.SettingsTest + "…";
|
||||||
|
IsTestingDb = true;
|
||||||
|
// On applique temporairement les valeurs saisies
|
||||||
|
var oldHost = _config.Database.Host;
|
||||||
|
var oldPort = _config.Database.Port;
|
||||||
|
var oldUser = _config.Database.User;
|
||||||
|
var oldPwd = _config.Database.EncryptedPassword;
|
||||||
|
var oldDb = _config.Database.Database;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_config.Database.Host = DbHost.Trim();
|
||||||
|
_config.Database.Port = DbPort > 0 ? DbPort : 3306;
|
||||||
|
_config.Database.User = DbUser.Trim();
|
||||||
|
_config.Database.EncryptedPassword = string.IsNullOrEmpty(DbPassword)
|
||||||
|
? null
|
||||||
|
: DatabasePasswordProtector.Protect(DbPassword);
|
||||||
|
_config.Database.Database = DbName.Trim();
|
||||||
|
|
||||||
|
var err = await _migrationService.TestConnectionAsync(CancellationToken.None);
|
||||||
|
DbConnectionStatus = err is null ? "✓ OK" : $"✗ {err}";
|
||||||
|
}
|
||||||
|
catch (Exception ex) { DbConnectionStatus = $"✗ {ex.Message}"; }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// Restaure : seul Save() doit écrire pour de bon
|
||||||
|
_config.Database.Host = oldHost;
|
||||||
|
_config.Database.Port = oldPort;
|
||||||
|
_config.Database.User = oldUser;
|
||||||
|
_config.Database.EncryptedPassword = oldPwd;
|
||||||
|
_config.Database.Database = oldDb;
|
||||||
|
IsTestingDb = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Re-joue les migrations bundled dans le _migrations/ de la version actuellement
|
||||||
|
/// installée la plus récente. Utile si l'auto-apply post-install avait échoué (ex.
|
||||||
|
/// XAMPP éteint à ce moment-là), ou pour rejouer manuellement après ajout d'une
|
||||||
|
/// nouvelle migration côté dev.
|
||||||
|
/// </summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ReplayMigrationsAsync()
|
||||||
|
{
|
||||||
|
var installed = _registry.Scan().OrderByDescending(v => v.Version).FirstOrDefault();
|
||||||
|
if (installed is null)
|
||||||
|
{
|
||||||
|
MigrationsReplayStatus = "Aucune version installée — installe d'abord une version avec _migrations/.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
IsReplayingMigrations = true;
|
||||||
|
MigrationsReplayStatus = $"Réapplication des migrations pour v{installed.Version}…";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var progress = new Progress<MigrationProgress>(mp =>
|
||||||
|
{
|
||||||
|
if (mp.Total > 0)
|
||||||
|
MigrationsReplayStatus = $"{mp.Done + 1}/{mp.Total} : {mp.CurrentFilename}";
|
||||||
|
});
|
||||||
|
// Il faut persister les éventuelles modifs DB avant le test (le service lit la config courante)
|
||||||
|
Save();
|
||||||
|
var result = await _migrationService.ApplyMigrationsAsync(
|
||||||
|
installed.FolderPath, progress, CancellationToken.None);
|
||||||
|
if (result.AllSucceeded)
|
||||||
|
{
|
||||||
|
MigrationsReplayStatus = result.AppliedCount > 0
|
||||||
|
? $"✓ {result.AppliedCount} migration(s) appliquée(s), {result.SkippedCount} déjà en place."
|
||||||
|
: $"✓ Tout est à jour ({result.SkippedCount} déjà appliquée(s))";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MigrationsReplayStatus = $"✗ Échec : {result.FailureMessage}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Replay migrations failed");
|
||||||
|
MigrationsReplayStatus = $"✗ Erreur : {ex.Message}";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
IsReplayingMigrations = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,6 +248,115 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!-- Base de données (XAMPP / MySQL) — config + test + replay -->
|
||||||
|
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="20" Margin="0,0,0,16">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsDatabase}"
|
||||||
|
FontSize="11" FontWeight="Bold"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
Margin="0,0,0,8" />
|
||||||
|
|
||||||
|
<Grid Margin="0,0,0,8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="100" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0" Margin="0,0,8,0">
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsDbHost}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,0,0,4" />
|
||||||
|
<TextBox Text="{Binding DbHost, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1">
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsDbPort}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,0,0,4" />
|
||||||
|
<TextBox Text="{Binding DbPort, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Margin="0,0,0,8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Grid.Column="0" Margin="0,0,8,0">
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsDbUser}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,0,0,4" />
|
||||||
|
<TextBox Text="{Binding DbUser, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1">
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsDbPassword}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,0,0,4" />
|
||||||
|
<TextBox Text="{Binding DbPassword, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8"
|
||||||
|
FontFamily="Consolas" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsDbName}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,0,0,4" />
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0"
|
||||||
|
Text="{Binding DbName, UpdateSourceTrigger=PropertyChanged}"
|
||||||
|
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8"
|
||||||
|
FontFamily="Consolas" />
|
||||||
|
<Button Grid.Column="1" Margin="8,0,0,0"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="{x:Static loc:Strings.SettingsTest}"
|
||||||
|
Command="{Binding TestDbCommand}"
|
||||||
|
IsEnabled="{Binding IsTestingDb, Converter={StaticResource InverseBoolToBool}}" />
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding DbConnectionStatus}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" Margin="0,8,0,0" TextWrapping="Wrap" />
|
||||||
|
|
||||||
|
<CheckBox IsChecked="{Binding DbAutoApplyMigrations}"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
Content="{x:Static loc:Strings.SettingsDbAutoMigrate}" />
|
||||||
|
|
||||||
|
<Grid Margin="0,12,0,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{Binding MigrationsReplayStatus}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontSize="12" VerticalAlignment="Center"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Style="{StaticResource SecondaryButton}"
|
||||||
|
Content="{x:Static loc:Strings.SettingsDbReplay}"
|
||||||
|
Command="{Binding ReplayMigrationsCommand}"
|
||||||
|
IsEnabled="{Binding IsReplayingMigrations, Converter={StaticResource InverseBoolToBool}}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Text="{x:Static loc:Strings.SettingsDbHint}"
|
||||||
|
Foreground="{StaticResource Brush.Text.Secondary}"
|
||||||
|
FontStyle="Italic" FontSize="11" Margin="0,8,0,0"
|
||||||
|
TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Logs / About -->
|
<!-- Logs / About -->
|
||||||
<Border Background="{StaticResource Brush.Bg.Card}"
|
<Border Background="{StaticResource Brush.Bg.Card}"
|
||||||
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
|
||||||
|
|||||||
@@ -138,6 +138,27 @@ public static class Strings
|
|||||||
public static string SettingsInstallation => T("INSTALLATION", "INSTALLATION", "安装", "การติดตั้ง", "التثبيت");
|
public static string SettingsInstallation => T("INSTALLATION", "INSTALLATION", "安装", "การติดตั้ง", "التثبيت");
|
||||||
public static string SettingsCache => T("CACHE DE TÉLÉCHARGEMENTS", "DOWNLOAD CACHE", "下载缓存", "แคชดาวน์โหลด", "ذاكرة التخزين المؤقت للتنزيلات");
|
public static string SettingsCache => T("CACHE DE TÉLÉCHARGEMENTS", "DOWNLOAD CACHE", "下载缓存", "แคชดาวน์โหลด", "ذاكرة التخزين المؤقت للتنزيلات");
|
||||||
public static string SettingsLogs => T("LOGS & APPLICATION", "LOGS & APP", "日志与应用", "บันทึกและแอป", "السجلات والتطبيق");
|
public static string SettingsLogs => T("LOGS & APPLICATION", "LOGS & APP", "日志与应用", "บันทึกและแอป", "السجلات والتطبيق");
|
||||||
|
public static string SettingsDatabase => T("BASE DE DONNÉES (XAMPP / MySQL)", "DATABASE (XAMPP / MySQL)", "数据库(XAMPP / MySQL)", "ฐานข้อมูล (XAMPP / MySQL)", "قاعدة البيانات (XAMPP / MySQL)");
|
||||||
|
public static string SettingsDbHost => T("Hôte", "Host", "主机", "โฮสต์", "المضيف");
|
||||||
|
public static string SettingsDbPort => T("Port", "Port", "端口", "พอร์ต", "المنفذ");
|
||||||
|
public static string SettingsDbUser => T("Utilisateur", "User", "用户", "ผู้ใช้", "المستخدم");
|
||||||
|
public static string SettingsDbPassword => T("Mot de passe", "Password", "密码", "รหัสผ่าน", "كلمة المرور");
|
||||||
|
public static string SettingsDbName => T("Nom de la base", "Database name", "数据库名称", "ชื่อฐานข้อมูล", "اسم قاعدة البيانات");
|
||||||
|
public static string SettingsDbAutoMigrate => T(
|
||||||
|
"Appliquer automatiquement les migrations à l'install d'une nouvelle version",
|
||||||
|
"Automatically apply migrations when installing a new version",
|
||||||
|
"安装新版本时自动应用迁移",
|
||||||
|
"ปรับใช้การโยกย้ายโดยอัตโนมัติเมื่อติดตั้งเวอร์ชันใหม่",
|
||||||
|
"تطبيق الترقيات تلقائياً عند تثبيت نسخة جديدة"
|
||||||
|
);
|
||||||
|
public static string SettingsDbReplay => T("🔁 Rejouer les migrations", "🔁 Replay migrations", "🔁 重新执行迁移", "🔁 เรียกใช้การโยกย้ายอีกครั้ง", "🔁 إعادة تشغيل الترقيات");
|
||||||
|
public static string SettingsDbHint => T(
|
||||||
|
"Le launcher applique automatiquement les scripts SQL bundled dans _migrations/ de chaque ZIP PROSERVE. Mot de passe stocké chiffré DPAPI.",
|
||||||
|
"The launcher automatically applies SQL scripts bundled in each PROSERVE ZIP's _migrations/ folder. Password stored DPAPI-encrypted.",
|
||||||
|
"启动器自动应用每个 PROSERVE ZIP 的 _migrations/ 文件夹中捆绑的 SQL 脚本。密码使用 DPAPI 加密存储。",
|
||||||
|
"Launcher จะปรับใช้สคริปต์ SQL ที่บรรจุใน _migrations/ ของ ZIP PROSERVE แต่ละชุดโดยอัตโนมัติ รหัสผ่านถูกเก็บแบบเข้ารหัส DPAPI",
|
||||||
|
"يطبق المُشغِّل تلقائياً نصوص SQL المرفقة في _migrations/ لكل ZIP من PROSERVE. كلمة المرور مُخزَّنة بتشفير DPAPI."
|
||||||
|
);
|
||||||
public static string SettingsLanguage => T("LANGUE", "LANGUAGE", "语言", "ภาษา", "اللغة");
|
public static string SettingsLanguage => T("LANGUE", "LANGUAGE", "语言", "ภาษา", "اللغة");
|
||||||
public static string SettingsLanguageHint => T(
|
public static string SettingsLanguageHint => T(
|
||||||
"Le launcher redémarrera pour appliquer le changement.",
|
"Le launcher redémarrera pour appliquer le changement.",
|
||||||
|
|||||||
Reference in New Issue
Block a user