Add translations to the Dashboard

This commit is contained in:
nicolas.dorier 2024-10-14 14:11:00 +09:00
parent 73a9835a27
commit c35af2dc69
No known key found for this signature in database
GPG Key ID: 6618763EF09186FE
31 changed files with 331 additions and 153 deletions

View File

@ -8,6 +8,14 @@ namespace BTCPayServer.Abstractions.Extensions;
public static class SetStatusMessageModelExtensions
{
public static void SetStatusSuccess(this ITempDataDictionary tempData, string statusMessage)
{
tempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
Message = statusMessage
});
}
public static void SetStatusMessageModel(this ITempDataDictionary tempData, StatusMessageModel statusMessage)
{
if (statusMessage == null)

View File

@ -16,11 +16,13 @@ using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using ExchangeSharp;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.Extensions.FileSystemGlobbing;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@ -349,6 +351,8 @@ retry:
{
defaultTranslatedKeys.Add(k);
}
AddLocalizers(defaultTranslatedKeys, txt);
}
// Go through all cshtml file, search for text-translate or ViewLocalizer usage
@ -360,21 +364,11 @@ retry:
{
var filePath = file.FullName;
var txt = File.ReadAllText(file.FullName);
foreach (string localizer in new[] { "ViewLocalizer", "StringLocalizer" })
{
if (txt.Contains(localizer))
{
var matches = Regex.Matches(txt, localizer + "\\[\"(.*?)\"[\\],]");
foreach (Match match in matches)
{
defaultTranslatedKeys.Add(match.Groups[1].Value);
}
}
}
AddLocalizers(defaultTranslatedKeys, txt);
filePath = filePath.Replace(Path.Combine(soldir.FullName, "BTCPayServer"), "/");
var item = engine.FileSystem.GetItem(filePath);
var node = (DocumentIntermediateNode)engine.Process(item).Items[typeof(DocumentIntermediateNode)];
var w = new TranslatedKeyNodeWalker(defaultTranslatedKeys, txt);
w.Visit(node);
@ -397,6 +391,24 @@ retry:
content += defaultTranslation.Substring(endIdx);
File.WriteAllText(path, content);
}
private static void AddLocalizers(List<string> defaultTranslatedKeys, string txt)
{
foreach (string localizer in new[] { "ViewLocalizer", "StringLocalizer" })
{
if (txt.Contains(localizer))
{
var matches = Regex.Matches(txt, localizer + "\\[\"(.*?)\"[\\],]");
foreach (Match match in matches)
{
var k = match.Groups[1].Value;
k = k.Replace("\\", "");
defaultTranslatedKeys.Add(k);
}
}
}
}
class DisplayNameWalker : CSharpSyntaxWalker
{
public List<string> Keys = new List<string>();

View File

@ -107,13 +107,13 @@
@if (ViewData.IsCategoryActive(typeof(WalletsNavPages), scheme.WalletId.ToString()) || ViewData.IsPageActive([WalletsNavPages.Settings], scheme.WalletId.ToString()) || ViewData.IsPageActive([StoreNavPages.OnchainSettings], categoryId))
{
<li class="nav-item nav-item-sub">
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId">Send</a>
<a id="WalletNav-Send" class="nav-link @ViewData.ActivePageClass([WalletsNavPages.Send, WalletsNavPages.PSBT], scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletSend" asp-route-walletId="@scheme.WalletId" text-translate="true">Send</a>
</li>
<li class="nav-item nav-item-sub">
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId">Receive</a>
<a id="WalletNav-Receive" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Receive, scheme.WalletId.ToString())" asp-area="" asp-controller="UIWallets" asp-action="WalletReceive" asp-route-walletId="@scheme.WalletId" text-translate="true">Receive</a>
</li>
<li class="nav-item nav-item-sub">
<a id="WalletNav-Settings" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Settings, scheme.WalletId.ToString()) @ViewData.ActivePageClass(StoreNavPages.OnchainSettings, categoryId)" asp-area="" asp-controller="UIStores" asp-action="WalletSettings" asp-route-cryptoCode="@scheme.WalletId.CryptoCode" asp-route-storeId="@scheme.WalletId.StoreId">Settings</a>
<a id="WalletNav-Settings" class="nav-link @ViewData.ActivePageClass(WalletsNavPages.Settings, scheme.WalletId.ToString()) @ViewData.ActivePageClass(StoreNavPages.OnchainSettings, categoryId)" asp-area="" asp-controller="UIStores" asp-action="WalletSettings" asp-route-cryptoCode="@scheme.WalletId.CryptoCode" asp-route-storeId="@scheme.WalletId.StoreId" text-translate="true">Settings</a>
</li>
<vc:ui-extension-point location="wallet-nav" model="@Model" />
}
@ -143,7 +143,7 @@
@if (ViewData.IsPageActive([StoreNavPages.Lightning, StoreNavPages.LightningSettings], $"{Model.Store.Id}-{scheme.CryptoCode}"))
{
<li class="nav-item nav-item-sub">
<a id="StoreNav-@(nameof(StoreNavPages.LightningSettings))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.LightningSettings)" asp-controller="UIStores" asp-action="LightningSettings" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@scheme.CryptoCode">Settings</a>
<a id="StoreNav-@(nameof(StoreNavPages.LightningSettings))" class="nav-link @ViewData.ActivePageClass(StoreNavPages.LightningSettings)" asp-controller="UIStores" asp-action="LightningSettings" asp-route-storeId="@Model.Store.Id" asp-route-cryptoCode="@scheme.CryptoCode" text-translate="true">Settings</a>
</li>
<vc:ui-extension-point location="lightning-nav" model="@Model"/>
}

View File

@ -5,7 +5,7 @@
}
<div id="StoreLightningBalance-@Model.Store.Id" class="widget store-lightning-balance">
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
<h6>Lightning Balance</h6>
<h6 text-translate="true">Lightning Balance</h6>
@if (Model.CryptoCode != Model.DefaultCurrency && Model.Balance != null)
{
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
@ -29,7 +29,7 @@
<div class="d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOffchain" data-sensitive>@Model.TotalOffchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> in channels
@ViewLocalizer["<span class=\"currency\">{0}</span> in channels", @Model.CryptoCode]
</span>
</div>
@ -41,7 +41,7 @@
@Model.Balance.OffchainBalance.Opening
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> opening channels
@ViewLocalizer["<span class=\"currency\">{0}</span> opening channels", @Model.CryptoCode]
</span>
</div>
}
@ -52,7 +52,7 @@
@Model.Balance.OffchainBalance.Local
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> local balance
@ViewLocalizer["<span class=\"currency\">{0}</span> local balance", @Model.CryptoCode]
</span>
</div>
}
@ -63,7 +63,7 @@
@Model.Balance.OffchainBalance.Remote
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> remote balance
@ViewLocalizer["<span class=\"currency\">{0}</span> remote balance", @Model.CryptoCode]
</span>
</div>
}
@ -74,7 +74,7 @@
@Model.Balance.OffchainBalance.Closing
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> closing channels
@ViewLocalizer["<span class=\"currency\">{0}</span> closing channels", @Model.CryptoCode]
</span>
</div>
}
@ -87,7 +87,7 @@
<div class="d-flex align-items-baseline gap-1">
<h3 class="d-inline-block me-1" data-balance="@Model.TotalOnchain" data-sensitive>@Model.TotalOnchain</h3>
<span class="text-secondary fw-semibold text-nowrap">
<span class="currency">@Model.CryptoCode</span> on-chain
@ViewLocalizer["<span class=\"currency\">{0}</span> on-chain", @Model.CryptoCode]
</span>
</div>
<div class="balance-details collapse" id="balanceDetailsOnchain">
@ -98,7 +98,7 @@
@Model.Balance.OnchainBalance.Confirmed
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> confirmed
@ViewLocalizer["<span class=\"currency\">{0}</span> confirmed", @Model.CryptoCode]
</span>
</div>
}
@ -109,7 +109,7 @@
@Model.Balance.OnchainBalance.Unconfirmed
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> unconfirmed
@ViewLocalizer["<span class=\"currency\">{0}</span> unconfirmed", @Model.CryptoCode]
</span>
</div>
}
@ -120,7 +120,7 @@
@Model.Balance.OnchainBalance.Reserved
</span>
<span class="text-secondary text-nowrap">
<span class="currency">@Model.CryptoCode</span> reserved
@ViewLocalizer["<span class=\"currency\">{0}</span> reserved", @Model.CryptoCode]
</span>
</div>
}
@ -132,7 +132,7 @@
{
<button class="d-inline-flex align-items-center btn btn-link text-primary fw-semibold p-0 mt-3 ms-n1" type="button" data-bs-toggle="collapse" data-bs-target=".balance-details" aria-expanded="false" aria-controls="balanceDetailsOffchain balanceDetailsOnchain">
<vc:icon symbol="caret-down"/>
<span class="ms-1">Details</span>
<span class="ms-1" text-translate="true">Details</span>
</button>
}
}
@ -140,7 +140,7 @@
{
<div class="loading d-flex justify-content-center p-3">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</div>
<script>

View File

@ -4,14 +4,15 @@
{
<div id="StoreLightningServices-@Model.Store.Id" class="widget store-lightning-services">
<header class="mb-4">
<h6>Lightning Services</h6>
<h6 text-translate="true">Lightning Services</h6>
<a
asp-controller="UIPublicLightningNodeInfo"
asp-action="ShowLightningNodeInfo"app-top-items
asp-route-cryptoCode="@Model.CryptoCode"
asp-route-storeId="@Model.Store.Id"
target="_blank"
id="PublicNodeInfo">
id="PublicNodeInfo"
text-translate="true">
Node Info
</a>
</header>

View File

@ -24,7 +24,7 @@
{
<div class="store-number">
<header>
<h6>Paid invoices in the last @Model.TimeframeDays days</h6>
<h6 text-translate="true">@ViewLocalizer["Paid invoices in the last {0} days", @Model.TimeframeDays]</h6>
@if (Model.PaidInvoices > 0)
{
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanViewInvoices">View All</a>
@ -34,14 +34,14 @@
</div>
<div class="store-number">
<header>
<h6>Payouts Pending</h6>
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanManagePullPayments">Manage</a>
<h6 text-translate="true">Payouts Pending</h6>
<a asp-controller="UIStorePullPayments" asp-action="Payouts" asp-route-storeId="@Model.Store.Id" permission="@Policies.CanManagePullPayments" text-translate="true">Manage</a>
</header>
<div class="h3">@Model.PayoutsPending</div>
</div>
<div class="store-number">
<header>
<h6>Refunds Issued</h6>
<h6 text-translate="true">Refunds Issued</h6>
</header>
<div class="h3">@Model.RefundsIssued</div>
</div>

View File

@ -6,17 +6,17 @@
<div class="widget store-recent-invoices" id="StoreRecentInvoices-@Model.Store.Id">
<header>
<h3>Recent Invoices</h3>
<h3 text-translate="true">Recent Invoices</h3>
@if (Model.Invoices.Any())
{
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id">View All</a>
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.Store.Id" text-translate="true">View All</a>
}
</header>
@if (Model.InitialRendering)
{
<div class="loading d-flex justify-content-center p-3">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</div>
<script>
@ -36,10 +36,10 @@
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="w-125px">Date</th>
<th class="text-nowrap">Invoice Id</th>
<th>Status</th>
<th class="text-end">Amount</th>
<th class="w-125px" text-translate="true">Date</th>
<th class="text-nowrap" text-translate="true">Invoice Id</th>
<th text-translate="true">Status</th>
<th class="text-end" text-translate="true">Amount</th>
</tr>
</thead>
<tbody>
@ -65,10 +65,10 @@
}
else
{
<p class="text-secondary my-3">
<p class="text-secondary my-3" text-translate="true">
There are no recent invoices.
</p>
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.Store.Id" class="fw-semibold">
<a asp-controller="UIInvoice" asp-action="CreateInvoice" asp-route-storeId="@Model.Store.Id" class="fw-semibold" text-translate="true">
Create Invoice
</a>
}

View File

@ -4,17 +4,17 @@
<div class="widget store-recent-transactions" id="StoreRecentTransactions-@Model.Store.Id">
<header>
<h3>Recent Transactions</h3>
<h3 text-translate="true">Recent Transactions</h3>
@if (Model.Transactions.Any())
{
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId">View All</a>
<a asp-controller="UIWallets" asp-action="WalletTransactions" asp-route-walletId="@Model.WalletId" text-translate="true">View All</a>
}
</header>
@if (Model.InitialRendering)
{
<div class="loading d-flex justify-content-center p-3">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden" text-translate="true">Loading...</span>
</div>
</div>
<script>
@ -34,10 +34,10 @@
<table class="table table-hover mb-0">
<thead>
<tr>
<th class="w-125px">Date</th>
<th>Transaction</th>
<th>Labels</th>
<th class="text-end">Amount</th>
<th class="w-125px" text-translate="true">Date</th>
<th text-translate="true">Transaction</th>
<th text-translate="true">Labels</th>
<th class="text-end" text-translate="true">Amount</th>
</tr>
</thead>
<tbody>
@ -90,7 +90,7 @@
}
else
{
<p class="text-secondary mt-3 mb-0">
<p class="text-secondary mt-3 mb-0" text-translate="true">
There are no recent transactions.
</p>
}

View File

@ -4,7 +4,7 @@
@inject BTCPayNetworkProvider NetworkProvider
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
<h6>Wallet Balance</h6>
<h6 text-translate="true">Wallet Balance</h6>
@if (Model.CryptoCode != Model.DefaultCurrency)
{
<div class="btn-group btn-group-sm gap-0 currency-toggle" role="group">
@ -39,7 +39,7 @@
{
<div class="ct-chart"></div>
}
else if (Model.Store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(Model.CryptoCode)) is null)
else if (Model.Store.GetPaymentMethodConfig(PaymentTypes.CHAIN.GetPaymentMethodId(Model.CryptoCode)) is null)
{
<p>
We would like to show you a chart of your balance but you have not yet <a href="@Url.Action("SetupWallet", "UIStores", new {storeId = Model.Store.Id, cryptoCode = Model.CryptoCode})">configured a wallet</a>.

View File

@ -18,8 +18,10 @@ using BTCPayServer.Services;
using BTCPayServer.Services.Rates;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using Newtonsoft.Json.Linq;
using MarkPayoutRequest = BTCPayServer.HostedServices.MarkPayoutRequest;
using PayoutData = BTCPayServer.Data.PayoutData;
@ -50,7 +52,10 @@ namespace BTCPayServer.Controllers
}
}
public IStringLocalizer StringLocalizer { get; }
public UIStorePullPaymentsController(BTCPayNetworkProvider btcPayNetworkProvider,
IStringLocalizer stringLocalizer,
PayoutMethodHandlerDictionary payoutHandlers,
CurrencyNameTable currencyNameTable,
DisplayFormatter displayFormatter,
@ -62,6 +67,7 @@ namespace BTCPayServer.Controllers
IAuthorizationService authorizationService)
{
_btcPayNetworkProvider = btcPayNetworkProvider;
StringLocalizer = stringLocalizer;
_payoutHandlers = payoutHandlers;
_currencyNameTable = currencyNameTable;
_displayFormatter = displayFormatter;
@ -85,7 +91,7 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a pull payment.",
Message = StringLocalizer["You must enable at least one payment method before creating a pull payment."],
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
@ -119,25 +125,25 @@ namespace BTCPayServer.Controllers
// them here to reflect user's selection so that they can correct their mistake
model.PayoutMethodsItem =
paymentMethodOptions.Select(id => new SelectListItem(id.ToString(), id.ToString(), false));
ModelState.AddModelError(nameof(model.PayoutMethods), "You need at least one payout method");
ModelState.AddModelError(nameof(model.PayoutMethods), StringLocalizer["You need at least one payout method"]);
}
if (_currencyNameTable.GetCurrencyData(model.Currency, false) is null)
{
ModelState.AddModelError(nameof(model.Currency), "Invalid currency");
ModelState.AddModelError(nameof(model.Currency), StringLocalizer["Invalid currency"]);
}
if (model.Amount <= 0.0m)
{
ModelState.AddModelError(nameof(model.Amount), "The amount should be more than zero");
ModelState.AddModelError(nameof(model.Amount), StringLocalizer["The amount should be more than zero"]);
}
if (model.Name.Length > 50)
{
ModelState.AddModelError(nameof(model.Name), "The name should be maximum 50 characters.");
ModelState.AddModelError(nameof(model.Name), StringLocalizer["The name should be maximum 50 characters."]);
}
var selectedPaymentMethodIds = model.PayoutMethods.Select(PayoutMethodId.Parse).ToArray();
if (!selectedPaymentMethodIds.All(id => paymentMethodOptions.Contains(id)))
{
ModelState.AddModelError(nameof(model.Name), "Not all payout methods are supported");
ModelState.AddModelError(nameof(model.Name), StringLocalizer["Not all payout methods are supported"]);
}
if (!ModelState.IsValid)
return View(model);
@ -156,7 +162,7 @@ namespace BTCPayServer.Controllers
});
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "Pull payment request created",
Message = StringLocalizer["Pull payment request created"],
Severity = StatusMessageModel.StatusSeverity.Success
});
return RedirectToAction(nameof(PullPayments), new { storeId });
@ -198,7 +204,7 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a pull payment.",
Message = StringLocalizer["You must enable at least one payment method before creating a pull payment."],
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });
@ -260,7 +266,7 @@ namespace BTCPayServer.Controllers
string pullPaymentId)
{
return View("Confirm",
new ConfirmModel("Archive pull payment", "Do you really want to archive the pull payment?", "Archive"));
new ConfirmModel(StringLocalizer["Archive pull payment"], StringLocalizer["Do you really want to archive the pull payment?"], "Archive"));
}
[HttpPost("stores/{storeId}/pull-payments/{pullPaymentId}/archive")]
@ -298,7 +304,7 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "No payout selected",
Message = StringLocalizer["No payout selected"],
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(Payouts),
@ -341,7 +347,7 @@ namespace BTCPayServer.Controllers
{
this.TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = $"Rate unavailable: {rateResult.EvaluatedRule}",
Message = StringLocalizer["Rate unavailable: {0}", rateResult.EvaluatedRule],
Severity = StatusMessageModel.StatusSeverity.Error
});
failed = true;
@ -379,7 +385,7 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts approved",
Message = StringLocalizer["Payouts approved"],
Severity = StatusMessageModel.StatusSeverity.Success
});
break;
@ -391,7 +397,7 @@ namespace BTCPayServer.Controllers
return await handler.InitiatePayment(payoutIds);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Paying via this payment method is not supported",
Message = StringLocalizer["Paying via this payment method is not supported"],
Severity = StatusMessageModel.StatusSeverity.Error
});
break;
@ -430,7 +436,7 @@ namespace BTCPayServer.Controllers
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts marked as paid",
Message = StringLocalizer["Payouts marked as paid"],
Severity = StatusMessageModel.StatusSeverity.Success
});
break;
@ -441,7 +447,7 @@ namespace BTCPayServer.Controllers
new PullPaymentHostedService.CancelRequest(payoutIds, new[] { storeId }));
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Message = "Payouts archived",
Message = StringLocalizer["Payouts archived"],
Severity = StatusMessageModel.StatusSeverity.Success
});
break;
@ -482,7 +488,7 @@ namespace BTCPayServer.Controllers
{
TempData.SetStatusMessageModel(new StatusMessageModel
{
Message = "You must enable at least one payment method before creating a payout.",
Message = StringLocalizer["You must enable at least one payment method before creating a payout."],
Severity = StatusMessageModel.StatusSeverity.Error
});
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new { storeId });

View File

@ -176,7 +176,7 @@ public partial class UIStoresController
vm.AvailableExchanges = sources;
var exchange = storeBlob.GetPreferredExchange(_defaultRules);
var chosenSource = sources.First(r => r.Id == exchange);
vm.Exchanges = UIUserStoresController.GetExchangesSelectList(_rateFactory, _defaultRules, storeBlob);
vm.Exchanges = _userStoresController.GetExchangesSelectList(storeBlob);
vm.PreferredExchange = vm.Exchanges.SelectedValue as string;
vm.PreferredResolvedExchange = chosenSource.Id;
vm.RateSource = chosenSource.Url;

View File

@ -58,6 +58,7 @@ public partial class UIStoresController : Controller
DefaultRulesCollection defaultRules,
EmailSenderFactory emailSenderFactory,
WalletFileParsers onChainWalletParsers,
UIUserStoresController userStoresController,
UriResolver uriResolver,
SettingsRepository settingsRepository,
CurrencyNameTable currencyNameTable,
@ -82,6 +83,7 @@ public partial class UIStoresController : Controller
_externalServiceOptions = externalServiceOptions;
_emailSenderFactory = emailSenderFactory;
_onChainWalletParsers = onChainWalletParsers;
_userStoresController = userStoresController;
_uriResolver = uriResolver;
_settingsRepository = settingsRepository;
_currencyNameTable = currencyNameTable;
@ -115,6 +117,7 @@ public partial class UIStoresController : Controller
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly WalletFileParsers _onChainWalletParsers;
private readonly UIUserStoresController _userStoresController;
private readonly UriResolver _uriResolver;
private readonly EventAggregator _eventAggregator;
private readonly IHtmlHelper _html;

View File

@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Client;
using BTCPayServer.Data;
@ -12,7 +13,9 @@ using BTCPayServer.Services.Stores;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Controllers
{
@ -21,6 +24,7 @@ namespace BTCPayServer.Controllers
public class UIUserStoresController : Controller
{
private readonly StoreRepository _repo;
private readonly IStringLocalizer StringLocalizer;
private readonly SettingsRepository _settingsRepository;
private readonly UserManager<ApplicationUser> _userManager;
private readonly DefaultRulesCollection _defaultRules;
@ -31,10 +35,12 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> userManager,
DefaultRulesCollection defaultRules,
StoreRepository storeRepository,
IStringLocalizer stringLocalizer,
RateFetcher rateFactory,
SettingsRepository settingsRepository)
{
_repo = storeRepository;
StringLocalizer = stringLocalizer;
_userManager = userManager;
_defaultRules = defaultRules;
_rateFactory = rateFactory;
@ -95,7 +101,7 @@ namespace BTCPayServer.Controllers
store.SetStoreBlob(blob);
await _repo.CreateStore(GetUserId(), store);
CreatedStoreId = store.Id;
TempData[WellKnownTempData.SuccessMessage] = "Store successfully created";
TempData.SetStatusSuccess(StringLocalizer["Store successfully created"]);
return RedirectToAction(nameof(UIStoresController.Index), "UIStores", new
{
storeId = store.Id
@ -109,7 +115,7 @@ namespace BTCPayServer.Controllers
var store = HttpContext.GetStoreData();
if (store == null)
return NotFound();
return View("Confirm", new ConfirmModel($"Delete store {store.StoreName}", "This store will still be accessible to users sharing it", "Delete"));
return View("Confirm", new ConfirmModel(StringLocalizer["Delete store {0}", store.StoreName], StringLocalizer["This store will still be accessible to users sharing it"], "Delete"));
}
[HttpPost("{storeId}/me/delete")]
@ -121,24 +127,23 @@ namespace BTCPayServer.Controllers
if (store == null)
return NotFound();
await _repo.RemoveStore(storeId, userId);
TempData[WellKnownTempData.SuccessMessage] = "Store removed successfully";
TempData.SetStatusSuccess(StringLocalizer["Store removed successfully"]);
return RedirectToAction(nameof(UIHomeController.Index), "UIHome");
}
private string GetUserId() => _userManager.GetUserId(User);
private SelectList GetExchangesSelectList(StoreBlob storeBlob) => GetExchangesSelectList(_rateFactory, _defaultRules, storeBlob);
internal static SelectList GetExchangesSelectList(RateFetcher rateFetcher, DefaultRulesCollection defaultRules, StoreBlob storeBlob)
internal SelectList GetExchangesSelectList(StoreBlob storeBlob)
{
if (storeBlob is null)
storeBlob = new StoreBlob();
var defaultExchange = defaultRules.GetRecommendedExchange(storeBlob.DefaultCurrency);
var exchanges = rateFetcher.RateProviderFactory
var defaultExchange = _defaultRules.GetRecommendedExchange(storeBlob.DefaultCurrency);
var exchanges = _rateFactory.RateProviderFactory
.AvailableRateProviders
.OrderBy(s => s.Id, StringComparer.OrdinalIgnoreCase)
.ToList();
var exchange = exchanges.First(e => e.Id == defaultExchange);
exchanges.Insert(0, new(null, $"Recommendation ({exchange.DisplayName})", ""));
exchanges.Insert(0, new(null, StringLocalizer["Recommendation ({0})", exchange.DisplayName], ""));
var chosen = exchanges.FirstOrDefault(f => f.Id == storeBlob.PreferredExchange) ?? exchanges.First();
return new SelectList(exchanges, nameof(chosen.Id), nameof(chosen.DisplayName), chosen.Id);
}

View File

@ -29,6 +29,7 @@ using BTCPayServer.Payments.Bitcoin;
using BTCPayServer.Payments.Lightning;
using BTCPayServer.Payouts;
using BTCPayServer.Security;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.Wallets;
@ -291,7 +292,17 @@ namespace BTCPayServer
}
}
#nullable enable
public static IServiceCollection AddDefaultTransactions(this IServiceCollection services, params string[] keyValues)
{
return services.AddDefaultTransactions(keyValues.Select(k => KeyValuePair.Create<string, string?>(k, string.Empty)).ToArray());
}
public static IServiceCollection AddDefaultTransactions(this IServiceCollection services, params KeyValuePair<string, string?>[] keyValues)
{
services.AddSingleton<IDefaultTransactionProvider>(new InMemoryDefaultTransactionProvider(keyValues));
return services;
}
#nullable restore
public static IServiceCollection AddUIExtension(this IServiceCollection services, string location, string partialViewName)
{
#pragma warning disable CS0618 // Type or member is obsolete

View File

@ -631,6 +631,7 @@ o.GetRequiredService<IEnumerable<IPaymentLinkExtension>>().ToDictionary(o => o.P
(IPaymentLinkExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentLinkExtension), new object[] { network, pmi }));
services.AddSingleton<ICheckoutModelExtension>(provider =>
(BitcoinCheckoutModelExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinCheckoutModelExtension), new object[] { network, pmi }));
services.AddDefaultTransactions(network.DisplayName);
services.AddSingleton<IPaymentMethodBitpayAPIExtension>(provider =>
(IPaymentMethodBitpayAPIExtension)ActivatorUtilities.CreateInstance(provider, typeof(BitcoinPaymentMethodBitpayAPIExtension), new object[] { pmi }));

View File

@ -7,6 +7,7 @@ using BTCPayServer.Models.InvoicingModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Invoices;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using NBitcoin;
using NBitcoin.DataEncoders;
@ -17,6 +18,7 @@ namespace BTCPayServer.Payments.Bitcoin
public const string CheckoutBodyComponentName = "BitcoinCheckoutBody";
private readonly PaymentMethodHandlerDictionary _handlers;
private readonly BTCPayNetwork _Network;
private readonly IStringLocalizer StringLocalizer;
private readonly DisplayFormatter _displayFormatter;
private readonly IPaymentLinkExtension paymentLinkExtension;
private readonly IPaymentLinkExtension? lnPaymentLinkExtension;
@ -26,6 +28,7 @@ namespace BTCPayServer.Payments.Bitcoin
public BitcoinCheckoutModelExtension(
PaymentMethodId paymentMethodId,
BTCPayNetwork network,
IStringLocalizer stringLocalizer,
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
DisplayFormatter displayFormatter,
PaymentMethodHandlerDictionary handlers)
@ -33,6 +36,7 @@ namespace BTCPayServer.Payments.Bitcoin
PaymentMethodId = paymentMethodId;
_handlers = handlers;
_Network = network;
StringLocalizer = stringLocalizer;
_displayFormatter = displayFormatter;
paymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
var lnPmi = PaymentTypes.LN.GetPaymentMethodId(network.CryptoCode);
@ -41,7 +45,7 @@ namespace BTCPayServer.Payments.Bitcoin
lnurlPaymentLinkExtension = paymentLinkExtensions.SingleOrDefault(p => p.PaymentMethodId == lnurlPmi);
_bech32Prefix = network.NBitcoinNetwork.GetBech32Encoder(Bech32Type.WITNESS_PUBKEY_ADDRESS, false) is { } enc ? Encoders.ASCII.EncodeData(enc.HumanReadablePart) : null;
}
public string DisplayName => _Network.DisplayName;
public string DisplayName => StringLocalizer[_Network.DisplayName];
public string Image => _Network.CryptoImagePath;
public string Badge => "";
public PaymentMethodId PaymentMethodId { get; }

View File

@ -6,6 +6,7 @@ using BTCPayServer.Services;
using Org.BouncyCastle.Crypto.Modes.Gcm;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Localization;
namespace BTCPayServer.Payments.Lightning
{
@ -14,27 +15,31 @@ namespace BTCPayServer.Payments.Lightning
public const string CheckoutBodyComponentName = "LightningCheckoutBody";
private readonly DisplayFormatter _displayFormatter;
IPaymentLinkExtension _PaymentLinkExtension;
private readonly bool isBTC;
public LNCheckoutModelExtension(
PaymentMethodId paymentMethodId,
BTCPayNetwork network,
DisplayFormatter displayFormatter,
IEnumerable<IPaymentLinkExtension> paymentLinkExtensions,
IStringLocalizer stringLocalizer,
PaymentMethodHandlerDictionary handlers)
{
Network = network;
_displayFormatter = displayFormatter;
StringLocalizer = stringLocalizer;
Handlers = handlers;
PaymentMethodId = paymentMethodId;
_PaymentLinkExtension = paymentLinkExtensions.Single(p => p.PaymentMethodId == PaymentMethodId);
var isBTC = PaymentTypes.LN.GetPaymentMethodId("BTC") == paymentMethodId;
DisplayName = isBTC ? "Lightning" : $"Lightning ({Network.DisplayName})";
isBTC = PaymentTypes.LN.GetPaymentMethodId("BTC") == paymentMethodId;
}
public BTCPayNetwork Network { get; }
public IStringLocalizer StringLocalizer { get; }
public PaymentMethodHandlerDictionary Handlers { get; }
public PaymentMethodId PaymentMethodId { get; }
public string DisplayName { get; }
public string DisplayName => isBTC ? StringLocalizer["Lightning"] : StringLocalizer["Lightning ({0})", Network.DisplayName];
public string Image => Network.LightningImagePath;
public string Badge => "⚡";
@ -50,7 +55,7 @@ namespace BTCPayServer.Payments.Lightning
if (context.Model.InvoiceBitcoinUrl is not null)
context.Model.InvoiceBitcoinUrlQR = $"lightning:{context.Model.InvoiceBitcoinUrl.ToUpperInvariant()?.Substring("LIGHTNING:".Length)}";
context.Model.PeerInfo = handler.ParsePaymentPromptDetails(paymentPrompt.Details).NodeInfo;
if (context.StoreBlob.LightningAmountInSatoshi && context.Model.PaymentMethodCurrency == "BTC")
if (context.StoreBlob.LightningAmountInSatoshi && isBTC)
{
BitcoinCheckoutModelExtension.PreparePaymentModelForAmountInSats(context.Model, paymentPrompt.Rate, _displayFormatter);
}

View File

@ -19,16 +19,35 @@ using static BTCPayServer.Services.LocalizerService;
namespace BTCPayServer.Services
{
public interface IDefaultTransactionProvider
{
Task<KeyValuePair<string, string?>[]> GetDefaultTransaction();
}
public class InMemoryDefaultTransactionProvider : IDefaultTransactionProvider
{
private readonly KeyValuePair<string, string?>[] _values;
public InMemoryDefaultTransactionProvider(KeyValuePair<string, string?>[] values)
{
_values = values;
}
public Task<KeyValuePair<string, string?>[]> GetDefaultTransaction()
{
return Task.FromResult(_values);
}
}
public class LocalizerService
{
public LocalizerService(
ILogger<LocalizerService> logger,
ApplicationDbContextFactory contextFactory,
ISettingsAccessor<PoliciesSettings> settingsAccessor)
ISettingsAccessor<PoliciesSettings> settingsAccessor,
IEnumerable<IDefaultTransactionProvider> defaultTransactionProviders)
{
_logger = logger;
_ContextFactory = contextFactory;
_settingsAccessor = settingsAccessor;
_defaultTransactionProviders = defaultTransactionProviders;
_LoadedTranslations = new LoadedTranslations(Translations.Default, Translations.Default, Translations.DefaultLanguage);
}
@ -39,6 +58,7 @@ namespace BTCPayServer.Services
private readonly ILogger<LocalizerService> _logger;
private readonly ApplicationDbContextFactory _ContextFactory;
private readonly ISettingsAccessor<PoliciesSettings> _settingsAccessor;
private readonly IEnumerable<IDefaultTransactionProvider> _defaultTransactionProviders;
/// <summary>
/// Load the translation of the server into memory
@ -69,7 +89,18 @@ namespace BTCPayServer.Services
{
dict_id = dictionaryName,
});
var fallback = new Translations(all.Where(a => a.fallback).Select(o => KeyValuePair.Create(o.sentence, o.translation)), Translations.Default);
var defaultDict = Translations.Default;
var loading = _defaultTransactionProviders.Select(d => d.GetDefaultTransaction()).ToArray();
Dictionary<string, string?> additionalDefault = new();
foreach (var defaultProvider in loading)
{
foreach (var kv in await defaultProvider)
{
additionalDefault.TryAdd(kv.Key, string.IsNullOrEmpty(kv.Value) ? kv.Key : kv.Value);
}
}
defaultDict = new Translations(additionalDefault, defaultDict);
var fallback = new Translations(all.Where(a => a.fallback).Select(o => KeyValuePair.Create(o.sentence, o.translation)), defaultDict);
var translations = new Translations(all.Where(a => !a.fallback).Select(o => KeyValuePair.Create(o.sentence, o.translation)), fallback);
return new LoadedTranslations(translations, fallback, dictionaryName);
}

View File

@ -14,7 +14,17 @@ namespace BTCPayServer.Services
{
"... on every payment": "",
"... only if the customer makes more than one payment for the invoice": "",
"<span class=\"currency\">{0}</span> closing channels": "",
"<span class=\"currency\">{0}</span> confirmed": "",
"<span class=\"currency\">{0}</span> in channels": "",
"<span class=\"currency\">{0}</span> local balance": "",
"<span class=\"currency\">{0}</span> on-chain": "",
"<span class=\"currency\">{0}</span> opening channels": "",
"<span class=\"currency\">{0}</span> remote balance": "",
"<span class=\"currency\">{0}</span> reserved": "",
"<span class=\"currency\">{0}</span> unconfirmed": "",
"A given currency pair match the most specific rule. If two rules are matching and are as specific, the first rule will be chosen.": "",
"A self-hosted, open-source bitcoin payment processor.": "",
"Access Tokens": "",
"Account": "",
"Account key": "",
@ -48,6 +58,7 @@ namespace BTCPayServer.Services
"Application": "",
"Apply the brand color to the store's backend as well": "",
"Approve": "",
"Archive pull payment": "",
"Archive this store": "",
"At Least One": "",
"At Least Ten": "",
@ -62,6 +73,7 @@ namespace BTCPayServer.Services
"blocks": "",
"Brand Color": "",
"Branding": "",
"BTCPay Server currently supports:": "",
"But now, what if you want to support <code>DOGE</code>? The problem with <code>DOGE</code> is that most exchange do not have any pair for it. But <code>bitpay</code> has a <code>DOGE_BTC</code> pair. <br />\r\n Luckily, the rule engine allow you to reference rules:": "",
"Buyer Email": "",
"Callback Notification URL": "",
@ -79,6 +91,8 @@ namespace BTCPayServer.Services
"Confirm password": "",
"Connect an existing wallet": "",
"Connect hardware&nbsp;wallet": "",
"Connect to a Lightning node": "",
"Connection configuration for your custom Lightning node:": "",
"Connection string": "",
"Consider the invoice paid even if the paid amount is … % less than expected": "",
"Consider the invoice settled when the payment transaction …": "",
@ -89,6 +103,7 @@ namespace BTCPayServer.Services
"Create": "",
"Create a new app": "",
"Create a new wallet": "",
"Create account": "",
"Create Account": "",
"Create Form": "",
"Create Invoice": "",
@ -97,6 +112,7 @@ namespace BTCPayServer.Services
"Create Store": "",
"Create Webhook": "",
"Create your account": "",
"Create your store": "",
"Crowdfund": "",
"Currency": "",
"Current password": "",
@ -107,18 +123,21 @@ namespace BTCPayServer.Services
"Custom Theme Extension Type": "",
"Custom Theme File": "",
"Dashboard": "",
"Date": "",
"days": "",
"Default currency": "",
"Default Currency Pairs": "",
"Default language on checkout": "",
"Default payment method on checkout": "",
"Default role for users on a new store": "",
"Delete store {0}": "",
"Delete this store": "",
"Derivation scheme": "",
"Derivation scheme format": "",
"Description": "",
"Description template of the lightning invoice": "",
"Destination Address": "",
"Details": "",
"Dictionaries": "",
"Dictionaries enable you to translate the BTCPay Server backend into different languages.": "",
"Dictionary": "",
@ -136,10 +155,14 @@ namespace BTCPayServer.Services
"Display Title": "",
"Disqus Shortname": "",
"Do not allow additional contributions after target has been reached": "",
"Do not photograph it. Do not store it digitally.": "",
"Do not photograph the recovery phrase, and do not store it digitally.": "",
"Do you really want to archive the pull payment?": "",
"Does not extend a BTCPay Server theme, fully custom": "",
"Domain": "",
"Domain name": "",
"Don't create UTXO change": "",
"Done": "",
"Email": "",
"Email address": "",
"Email confirmation required": "",
@ -188,27 +211,40 @@ namespace BTCPayServer.Services
"However, explicitely setting specific pairs like this can be a bit difficult. Instead, you can define a rule <code>X_X</code> which will match any currency pair. The following example will use <code>kraken</code> for getting the rate of any currency pair.": "",
"I don't have a wallet": "",
"I have a wallet": "",
"I have written down my recovery phrase and stored it in a secure location": "",
"If a translation isnt available in the new dictionary, it will be searched in the fallback.": "",
"If you lose it or write it down incorrectly, you may permanently lose access to your funds.": "",
"If you lose it or write it down incorrectly, you will permanently lose access to your funds.": "",
"Image": "",
"Import {0} Wallet": "",
"Import an existing hardware or software wallet": "",
"Import wallet file": "",
"Import your public keys using our Vault application": "",
"Input the key string manually": "",
"Invalid currency": "",
"Invitation URL": "",
"Invoice currency": "",
"Invoice expires if the full amount has not been paid after …": "",
"Invoice Id": "",
"Invoice metadata": "",
"Invoices": "",
"Is administrator?": "",
"Is signing key": "",
"Is unconfirmed": "",
"It is secure, private, censorship-resistant and free.": "",
"It is worth noting that the inverses of those pairs are automatically supported as well.<br />\r\n It means that the rule <code>USD_DOGE = 1 / DOGE_USD</code> implicitely exists.": "",
"Item Description": "",
"Keypad": "",
"Labels": "",
"Let's get started": "",
"Lightning": "",
"Lightning ({0})": "",
"Lightning Address": "",
"Lightning Balance": "",
"Lightning node (LNURL Auth)": "",
"Lightning Services": "",
"LNURL Classic Mode": "",
"Loading...": "",
"Local File System": "",
"Log in": "",
"Login Codes": "",
@ -217,6 +253,7 @@ namespace BTCPayServer.Services
"Logs": "",
"Maintenance": "",
"Make Crowdfund Public": "",
"Manage": "",
"Manage Account": "",
"Manage Plugins": "",
"Master fingerprint": "",
@ -228,11 +265,15 @@ namespace BTCPayServer.Services
"Never add network fee": "",
"New password": "",
"Next": "",
"No payout selected": "",
"No scope": "",
"Node Info": "",
"Non-admins can access the User Creation API Endpoint": "",
"Non-admins can create Hot Wallets for their Store": "",
"Non-admins can import Hot Wallets for their Store": "",
"Non-admins can use the Internal Lightning Node for their Store": "",
"Non-admins cannot access the User Creation API Endpoint": "",
"Not all payout methods are supported": "",
"Not recommended": "",
"Notification Email": "",
"Notification URL": "",
@ -241,18 +282,27 @@ namespace BTCPayServer.Services
"Optional seed passphrase": "",
"Order Id": "",
"Override the block explorers used": "",
"Paid invoices in the last {0} days": "",
"Pair to": "",
"Password": "",
"Password (leave blank to generate invite-link)": "",
"Pay Button": "",
"Paying via this payment method is not supported": "",
"PayJoin BIP21": "",
"Payment": "",
"Payment invalid if transactions fails to confirm … after invoice expiration": "",
"Payment Requests": "",
"Payments": "",
"Payout Methods": "",
"Payout Processors": "",
"Payouts": "",
"Payouts approved": "",
"Payouts archived": "",
"Payouts marked as paid": "",
"Payouts Pending": "",
"Permissions": "",
"Please enable JavaScript for this option to be available": "",
"Please make sure to also write down your passphrase.": "",
"Please note that creating a hot wallet is not supported by this instance for non administrators.": "",
"Plugin server": "",
"Plugins": "",
@ -261,6 +311,7 @@ namespace BTCPayServer.Services
"Policies": "",
"Preferred Price Source": "",
"Print display": "",
"Process approved payouts instantly": "",
"Product list": "",
"Product list with cart": "",
"Profile Picture": "",
@ -268,15 +319,22 @@ namespace BTCPayServer.Services
"PSBT content": "",
"PSBT to combine with…": "",
"Public Key": "",
"Pull payment request created": "",
"Pull Payments": "",
"Rate Rules": "",
"Rate script allows you to express precisely how you want to calculate rates for currency pairs.": "",
"Rate unavailable: {0}": "",
"Rates": "",
"Receive": "",
"Recent Invoices": "",
"Recent Transactions": "",
"Recommendation ({0})": "",
"Recommended": "",
"Recommended fee confirmation target blocks": "",
"Recovery Code": "",
"Redirect invoice to redirect url automatically after paid": "",
"Redirect URL": "",
"Refunds Issued": "",
"Register": "",
"Remember me": "",
"Remember this machine": "",
@ -298,10 +356,12 @@ namespace BTCPayServer.Services
"Scope": "",
"Scripting": "",
"Search engines can index this site": "",
"Secure your recovery phrase": "",
"Security device (FIDO2)": "",
"Select": "",
"Select the Default Currency during Store Creation": "",
"Select the payout method used for refund": "",
"Send": "",
"Send invitation email": "",
"Send test webhook": "",
"Server Name": "",
@ -309,6 +369,8 @@ namespace BTCPayServer.Services
"Services": "",
"Set Password": "",
"Set to default settings": "",
"Set up a Lightning node": "",
"Set up a wallet": "",
"Settings": "",
"Setup {0} Wallet": "",
"Shop Name": "",
@ -326,28 +388,46 @@ namespace BTCPayServer.Services
"Specify the amount and currency for the refund": "",
"Start date": "",
"Starting index": "",
"Status": "",
"Store": "",
"Store Id": "",
"Store Name": "",
"Store removed successfully": "",
"Store Settings": "",
"Store Speed Policy": "",
"Store successfully created": "",
"Store Website": "",
"Store: {0}": "",
"Submit": "",
"Subtract fees from amount": "",
"Support URL": "",
"Supported by BlueWallet, Cobo Vault, Passport and Specter DIY": "",
"Supported Transaction Currencies": "",
"Target Amount": "",
"Test connection": "",
"Test Email": "",
"Test Results:": "",
"Testing": "",
"Text to display in the tip input": "",
"Text to display on buttons allowing the user to enter a custom amount": "",
"Text to display on each button for items with a specific price": "",
"The amount should be more than zero": "",
"The combination of words below are called your recovery phrase.\r\n The recovery phrase allows you to access and restore your wallet.\r\n Write them down on a piece of paper in the exact order:": "",
"The following methods assume that you already have an existing&nbsp;wallet created and backed up.": "",
"The name should be maximum 50 characters.": "",
"The recommended price source gets chosen based on the default currency.": "",
"The recovery phrase is a backup that allows you to restore your wallet in case of a server crash.": "",
"The recovery phrase will also be stored on the server as a hot wallet.": "",
"The recovery phrase will be permanently erased from the server.": "",
"The script language is composed of several rules composed of a currency pair and a mathematic expression.\r\n The example below will use <code>kraken</code> for both <code>LTC_USD</code> and <code>BTC_USD</code> pairs.": "",
"Theme": "",
"There are no recent invoices.": "",
"There are no recent transactions.": "",
"This store is ready to accept transactions, good job!": "",
"This store will still be accessible to users sharing it": "",
"Tip percentage amounts (comma separated)": "",
"To start accepting payments, set up a wallet or a Lightning node.": "",
"Transaction": "",
"Translations": "",
"Two-Factor Authentication": "",
"Unarchive this store": "",
@ -357,14 +437,19 @@ namespace BTCPayServer.Services
"Upload a file exported from your wallet": "",
"Upload PSBT from file…": "",
"Url of the Dynamic DNS service you are using": "",
"Use custom node": "",
"Use custom theme": "",
"Use internal node": "",
"Use SSL": "",
"User can input custom amount": "",
"User can input discount in %": "",
"Users": "",
"Using the BTCPay Server internal node for this store requires no further configuration. Click the save button below to start accepting Bitcoin through the Lightning Network.": "",
"UTXOs to spend from": "",
"Verification Code": "",
"View All": "",
"View-Only Wallet File": "",
"Wallet Balance": "",
"Wallet file": "",
"Wallet file content": "",
"Wallet Keys File": "",
@ -376,8 +461,12 @@ namespace BTCPayServer.Services
"Webhooks": "",
"Welcome to {0}": "",
"With <code>DOGE_USD</code> will be expanded to <code>bitpay(DOGE_BTC) * kraken(BTC_USD)</code>. And <code>DOGE_CAD</code> will be expanded to <code>bitpay(DOGE_BTC) * ndax(BTC_CAD)</code>. <br />\r\n However, we advise you to write it that way to increase coverage so that <code>DOGE_BTC</code> is also supported:": "",
"You must enable at least one payment method before creating a payout.": "",
"You must enable at least one payment method before creating a pull payment.": "",
"You need at least one payout method": "",
"You really should not type your seed into a device that is connected to the internet.": "",
"Your dynamic DNS hostname": "",
"Your instance administrator has disabled the use of the Internal node for non-admin users.": "",
"Zero Confirmation": ""
}
""";

View File

@ -1,6 +1,6 @@
@model ErrorViewModel
@{
ViewData["Title"] = "Error";
ViewData["Title"] = ViewLocalizer["Error"];
}
<h1 class="text-danger">

View File

@ -10,7 +10,7 @@
<li class="nav-item" permission="@Policies.CanModifyStoreSettings">
<a asp-area="" asp-controller="UILNURL" asp-action="EditLightningAddress" asp-route-storeId="@store.Id" class="nav-link @ViewData.ActivePageClass("LightningAddress", nameof(StoreNavPages))" id="StoreNav-LightningAddress">
<vc:icon symbol="nav-lightning-address "/>
<span>Lightning Address</span>
<span text-translate="true">Lightning Address</span>
</a>
</li>
}

View File

@ -53,8 +53,8 @@
@if (ViewBag.ShowLeadText)
{
<p class="lead">
<span class="d-sm-block">A self-hosted, open-source bitcoin payment processor.</span>
<span class="d-sm-block">It is secure, private, censorship-resistant and free.</span>
<span class="d-sm-block" text-translate="true">A self-hosted, open-source bitcoin payment processor.</span>
<span class="d-sm-block" text-translate="true">It is secure, private, censorship-resistant and free.</span>
</p>
}

View File

@ -1,17 +1,17 @@
@model CheatPermissionsViewModel
@{
ViewData["Title"] = "Permissions";
ViewData["Title"] = ViewLocalizer["Permissions"];
Layout = "_LayoutSignedOut";
}
@if (Model.StoreId is not null)
{
<h1>Store: @Model.StoreId</h1>
<h1 text-translate="true">@ViewLocalizer["Store: {0}", @Model.StoreId]</h1>
}
else
{
<h1>No scope</h1>
<h1 text-translate="true">No scope</h1>
}
<ul>

View File

@ -1,7 +1,7 @@
@model RegisterViewModel
@inject BTCPayServer.Services.BTCPayServerEnvironment env
@{
ViewData["Title"] = "Create account";
ViewData["Title"] = ViewLocalizer["Create account"];
ViewBag.ShowLeadText = true;
Layout = "_LayoutSignedOut";
}
@ -36,7 +36,7 @@
</div>
}
<div class="form-group mt-4">
<button type="submit" class="btn btn-primary btn-lg w-100" id="RegisterButton">Create account</button>
<button type="submit" class="btn btn-primary btn-lg w-100" id="RegisterButton" text-translate="true">Create account</button>
</div>
</fieldset>
</form>

View File

@ -30,13 +30,13 @@
<vc:icon symbol="warning" />
</div>
<div class="lead text-center">
<h1 class="text-center text-warning mb-3">
Secure your recovery&nbsp;phrase
<h1 class="text-center text-warning mb-3" text-translate="true">
Secure your recovery phrase
</h1>
</div>
</div>
<div class="lead text-center">
<p class="mb-0">
<p class="mb-0" text-translate="true">
The combination of words below are called your recovery phrase.
The recovery phrase allows you to access and restore your wallet.
Write them down on a piece of paper in the exact order:
@ -54,41 +54,41 @@
@if (Model.IsStored)
{
<p>
<span>The recovery phrase is a backup that allows you to restore your wallet in case of a server crash.</span>
<span>If you lose it or write it down incorrectly, you may permanently lose access to your funds.</span>
<span>Do not photograph it. Do not store it digitally.</span>
<span text-translate="true">The recovery phrase is a backup that allows you to restore your wallet in case of a server crash.</span>
<span text-translate="true">If you lose it or write it down incorrectly, you may permanently lose access to your funds.</span>
<span text-translate="true">Do not photograph it. Do not store it digitally.</span>
</p>
<p class="text-warning">
<strong>The recovery phrase will also be stored on the server as a hot wallet.</strong>
<strong text-translate="true">The recovery phrase will also be stored on the server as a hot wallet.</strong>
</p>
}
else
{
<p>
<span>If you lose it or write it down incorrectly, you will permanently lose access to your funds.</span>
<span>Do not photograph the recovery phrase, and do not store it digitally.</span>
<span text-translate="true">If you lose it or write it down incorrectly, you will permanently lose access to your funds.</span>
<span text-translate="true">Do not photograph the recovery phrase, and do not store it digitally.</span>
</p>
<br />
<p class="text-warning">
<strong>The recovery phrase will be permanently erased from the server.</strong>
<strong text-translate="true">The recovery phrase will be permanently erased from the server.</strong>
</p>
}
@if (!string.IsNullOrEmpty(Model.Passphrase))
{
<p class="mt-3 mb-0">Please make sure to also write down your passphrase.</p>
<p class="mt-3 mb-0" text-translate="true">Please make sure to also write down your passphrase.</p>
}
</div>
@if (Model.RequireConfirm)
{
<form id="RecoveryConfirmation" action="@Url.EnsureLocal(Model.ReturnUrl, Context.Request)" class="position-relative d-flex align-items-start justify-content-center" style="padding:20px 0 100px" rel="noreferrer noopener">
<label class="form-check-label lead order-2" for="confirm">I have written down my recovery phrase and stored it in a secure location</label>
<label class="form-check-label lead order-2" for="confirm" text-translate="true">I have written down my recovery phrase and stored it in a secure location</label>
<input type="checkbox" class="me-3 order-1 form-check-input" id="confirm" style="margin-top:.35rem;flex-shrink:0">
<button type="submit" class="btn btn-primary btn-lg px-5 order-3" id="submit">Done</button>
<button type="submit" class="btn btn-primary btn-lg px-5 order-3" disabled>Done</button>
<button type="submit" class="btn btn-primary btn-lg px-5 order-3" id="submit" text-translate="true">Done</button>
<button type="submit" class="btn btn-primary btn-lg px-5 order-3" disabled text-translate="true">Done</button>
</form>
}
else
{
<a href="@Url.EnsureLocal(Model.ReturnUrl, Context.Request)" class="btn btn-primary btn-lg mt-3 px-5 order-3" id="proceed" rel="noreferrer noopener">Done</a>
<a href="@Url.EnsureLocal(Model.ReturnUrl, Context.Request)" class="btn btn-primary btn-lg mt-3 px-5 order-3" id="proceed" rel="noreferrer noopener" text-translate="true">Done</a>
}
</main>

View File

@ -1,6 +1,6 @@
@model BTCPayServer.Models.NotificationViewModels.NotificationIndexViewModel
@{
ViewData["Title"] = "Notifications";
ViewData["Title"] = ViewLocalizer["Notifications"];
string status = ViewBag.Status;
var statusFilterCount = CountArrayFilter("type");
var storesFilterCount = CountArrayFilter("storeid");

View File

@ -6,7 +6,7 @@
@model BTCPayServer.Models.PaymentRequestViewModels.ListPaymentRequestsViewModel
@{
Layout = "_Layout";
ViewData["Title"] = "Payment Requests";
ViewData["Title"] = ViewLocalizer["Payment Requests"];
var storeId = Context.GetStoreData().Id;
var statusFilterCount = CountArrayFilter("status") + (HasBooleanFilter("includearchived") ? 1 : 0);
}

View File

@ -9,8 +9,8 @@
@using BTCPayServer.Client
@model StoreDashboardViewModel
@{
BTCPayServer.Plugins.PluginExceptionHandler.SetDisablePluginIfCrash(Context);
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
BTCPayServer.Plugins.PluginExceptionHandler.SetDisablePluginIfCrash(Context);
ViewData.SetActivePage(StoreNavPages.Dashboard, Model.StoreName, Model.StoreId);
var store = ViewContext.HttpContext.GetStoreData();
}
@ -50,61 +50,61 @@
};
</script>
<div id="Dashboard" class="mt-4">
<vc:ui-extension-point location="dashboard" model="@Model"/>
<vc:ui-extension-point location="dashboard" model="@Model" />
@if (Model.WalletEnabled)
{
<vc:store-wallet-balance store="@store"/>
<vc:store-wallet-balance store="@store" />
}
else
{
<div class="widget setup-guide">
<header>
<h5 class="mb-4 text-muted">This store is ready to accept transactions, good job!</h5>
<h5 class="mb-4 text-muted" text-translate="true">This store is ready to accept transactions, good job!</h5>
</header>
<div class="list-group" id="SetupGuide">
<div class="list-group-item d-flex align-items-center" id="SetupGuide-LightningDone">
<vc:icon symbol="done"/>
<vc:icon symbol="done" />
<div class="content">
<h5 class="mb-0 text-success">Set up a Lightning node</h5>
<h5 class="mb-0 text-success" text-translate="true">Set up a Lightning node</h5>
</div>
</div>
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center">
<vc:icon symbol="wallet-new"/>
<vc:icon symbol="wallet-new" />
<div class="content">
<h5 class="mb-0">Set up a wallet</h5>
<h5 class="mb-0" text-translate="true">Set up a wallet</h5>
</div>
<vc:icon symbol="caret-right"/>
<vc:icon symbol="caret-right" />
</a>
</div>
</div>
}
<vc:store-numbers vm="@(new StoreNumbersViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
<vc:store-numbers vm="@(new StoreNumbersViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })" />
@if (Model.LightningEnabled)
{
<vc:store-lightning-balance vm="@(new StoreLightningBalanceViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
<vc:store-lightning-services vm="@(new StoreLightningServicesViewModel { Store = store, CryptoCode = Model.CryptoCode })" permission="@Policies.CanModifyServerSettings"/>
<vc:store-lightning-balance vm="@(new StoreLightningBalanceViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })" />
<vc:store-lightning-services vm="@(new StoreLightningServicesViewModel { Store = store, CryptoCode = Model.CryptoCode })" permission="@Policies.CanModifyServerSettings" />
}
@if (Model.WalletEnabled)
{
<vc:store-recent-transactions vm="@(new StoreRecentTransactionsViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
<vc:store-recent-transactions vm="@(new StoreRecentTransactionsViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })" />
}
<vc:store-recent-invoices vm="@(new StoreRecentInvoicesViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })"/>
<vc:store-recent-invoices vm="@(new StoreRecentInvoicesViewModel { Store = store, CryptoCode = Model.CryptoCode, InitialRendering = true })" />
@foreach (var app in Model.Apps)
{
<vc:app-sales app-id="@app.Id" app-type="@app.AppType" />
<vc:app-top-items app-id="@app.Id" app-type="@app.AppType" />
<vc:app-sales app-id="@app.Id" app-type="@app.AppType" />
<vc:app-top-items app-id="@app.Id" app-type="@app.AppType" />
}
</div>
}
else
{
<p class="lead text-secondary mt-2">To start accepting payments, set up a wallet or a Lightning node.</p>
<p class="lead text-secondary mt-2" text-translate="true">To start accepting payments, set up a wallet or a Lightning node.</p>
<div class="list-group" id="SetupGuide">
<div class="list-group-item d-flex align-items-center" id="SetupGuide-StoreDone">
<vc:icon symbol="done"/>
<vc:icon symbol="done" />
<div class="content">
<h5 class="mb-0 text-success">Create your store</h5>
<h5 class="mb-0 text-success" text-translate="true">Create your store</h5>
</div>
</div>
@if (Model.Network is BTCPayNetwork)
@ -113,40 +113,41 @@ else
@if (!Model.WalletEnabled)
{
<a asp-controller="UIStores" asp-action="SetupWallet" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="SetupGuide-Wallet" class="list-group-item list-group-item-action d-flex align-items-center order-1">
<vc:icon symbol="wallet-new"/>
<vc:icon symbol="wallet-new" />
<div class="content">
<h5 class="mb-0">Set up a wallet</h5>
<h5 class="mb-0" text-translate="true">Set up a wallet</h5>
</div>
<vc:icon symbol="caret-right"/>
<vc:icon symbol="caret-right" />
</a>
}
else
{
<div class="list-group-item d-flex align-items-center" id="SetupGuide-WalletDone">
<vc:icon symbol="done"/>
<vc:icon symbol="done" />
<div class="content">
<h5 class="mb-0 text-success">Set up a wallet</h5>
<h5 class="mb-0 text-success" text-translate="true">Set up a wallet</h5>
</div>
</div>
}
}
@if (Model.LightningSupported) {
@if (Model.LightningSupported)
{
if (!Model.LightningEnabled)
{
<a asp-controller="UIStores" asp-action="SetupLightningNode" asp-route-storeId="@Model.StoreId" asp-route-cryptoCode="@Model.CryptoCode" id="SetupGuide-Lightning" class="list-group-item list-group-item-action d-flex align-items-center order-1">
<vc:icon symbol="wallet-new"/>
<vc:icon symbol="wallet-new" />
<div class="content">
<h5 class="mb-0">Set up a Lightning node</h5>
<h5 class="mb-0" text-translate="true">Set up a Lightning node</h5>
</div>
<vc:icon symbol="caret-right"/>
<vc:icon symbol="caret-right" />
</a>
}
else
{
<div class="list-group-item d-flex align-items-center" id="SetupGuide-LightningDone">
<vc:icon symbol="done"/>
<vc:icon symbol="done" />
<div class="content">
<h5 class="mb-0 text-success">Set up a Lightning node</h5>
<h5 class="mb-0 text-success" text-translate="true">Set up a Lightning node</h5>
</div>
</div>
}

View File

@ -1,7 +1,7 @@
@model LightningNodeViewModel
@{
Layout = "_LayoutWalletSetup.cshtml";
ViewData.SetActivePage(StoreNavPages.LightningSettings, "Connect to a Lightning node", Context.GetStoreData().Id);
ViewData.SetActivePage(StoreNavPages.LightningSettings, StringLocalizer["Connect to a Lightning node"], Context.GetStoreData().Id);
}
@section PageHeadContent {
@ -28,10 +28,10 @@
<form method="post" class="mt-n2 text-center">
<div id="LightningNodeTypeTablist" class="nav btcpay-pills align-items-center justify-content-center mb-3" role="tablist">
<input asp-for="LightningNodeType" value="@LightningNodeType.Internal" type="radio" id="LightningNodeType-@LightningNodeType.Internal" data-bs-toggle="pill" data-bs-target="#InternalSetup" role="tab" aria-controls="InternalSetup" aria-selected="@(Model.LightningNodeType == LightningNodeType.Internal ? "true" : "false")" class="@(Model.LightningNodeType == LightningNodeType.Internal ? "active" : "")" disabled="@(!Model.CanUseInternalNode)">
<label asp-for="LightningNodeType" for="@($"LightningNodeType-{LightningNodeType.Internal}")">Use internal node</label>
<label asp-for="LightningNodeType" for="@($"LightningNodeType-{LightningNodeType.Internal}")" text-translate="true">Use internal node</label>
<input asp-for="LightningNodeType" value="@LightningNodeType.Custom" type="radio" id="LightningNodeType-@LightningNodeType.Custom" data-bs-toggle="pill" data-bs-target="#CustomSetup" role="tab" aria-controls="CustomSetup" aria-selected="@(Model.LightningNodeType == LightningNodeType.Custom ? "true" : "false")" class="@(Model.LightningNodeType == LightningNodeType.Custom ? "active" : "")">
<label asp-for="LightningNodeType" for="@($"LightningNodeType-{LightningNodeType.Custom}")">Use custom node</label>
<label asp-for="LightningNodeType" for="@($"LightningNodeType-{LightningNodeType.Custom}")" text-translate="true">Use custom node</label>
<vc:ui-extension-point location="ln-payment-method-setup-tabhead" model="@Model"/>
</div>
@ -40,24 +40,24 @@
<div id="InternalSetup" class="pt-3 tab-pane fade @(Model.LightningNodeType == LightningNodeType.Internal ? "show active" : "")" role="tabpanel" aria-labelledby="LightningNodeType-@LightningNodeType.Internal">
@if (Model.CanUseInternalNode)
{
<p class="mb-4">Using the BTCPay Server internal node for this store requires no further configuration. Click the save button below to start accepting Bitcoin through the Lightning Network.</p>
<p class="mb-4" text-translate="true">Using the BTCPay Server internal node for this store requires no further configuration. Click the save button below to start accepting Bitcoin through the Lightning Network.</p>
}
else
{
<p class="mb-4">Your instance administrator has disabled the use of the Internal node for non-admin users.</p>
<p class="mb-4" text-translate="true">Your instance administrator has disabled the use of the Internal node for non-admin users.</p>
}
</div>
<div id="CustomSetup" class="pt-3 tab-pane fade @(Model.LightningNodeType == LightningNodeType.Custom ? "show active" : "")" role="tabpanel" aria-labelledby="LightningNodeType-@LightningNodeType.Custom">
<div class="form-group">
<label asp-for="ConnectionString" class="form-label">Connection configuration for your custom Lightning node:</label>
<label asp-for="ConnectionString" class="form-label" text-translate="true">Connection configuration for your custom Lightning node:</label>
<div class="d-sm-flex">
<input asp-for="ConnectionString" class="form-control mb-2 me-2" placeholder="type=…;server=…;" value="@(Model.LightningNodeType == LightningNodeType.Internal ? "" : Model.ConnectionString)"/>
<button id="test" name="command" type="submit" value="test" class="btn btn-secondary text-nowrap mb-2">Test connection</button>
<button id="test" name="command" type="submit" value="test" class="btn btn-secondary text-nowrap mb-2" text-translate="true">Test connection</button>
</div>
<span asp-validation-for="ConnectionString" class="text-danger"></span>
</div>
<vc:ui-extension-point location="ln-payment-method-setup-custom" model="@Model"/>
<p class="mt-4 mb-2">BTCPay Server currently supports:</p>
<p class="mt-4 mb-2" text-translate="true">BTCPay Server currently supports:</p>
<div class="accordion" id="CustomNodeSupport">
<div class="accordion-item">
<h2 class="accordion-header" id="CustomNodeCLightningHeader">

View File

@ -18,7 +18,7 @@
<div class="form-group">
<label asp-for="PreferredExchange" class="form-label" data-required></label>
<select asp-for="PreferredExchange" asp-items="Model.Exchanges" class="form-select w-300px"></select>
<div class="form-text mt-2 only-for-js">The recommended price source gets chosen based on the default currency.</div>
<div class="form-text mt-2 only-for-js" text-translate="true">The recommended price source gets chosen based on the default currency.</div>
<span asp-validation-for="PreferredExchange" class="text-danger"></span>
</div>
<div class="form-group mt-4">

View File

@ -11,6 +11,7 @@
@using Microsoft.AspNetCore.Routing;
@using BTCPayServer.Abstractions.Extensions;
@inject Microsoft.AspNetCore.Mvc.Localization.ViewLocalizer ViewLocalizer
@inject Microsoft.Extensions.Localization.IStringLocalizer StringLocalizer
@inject Safe Safe
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, BTCPayServer