Refactor: Move translations classes in its own plugin
What changed, and why it matters
This commit is a routine code reorganization: it moves BTCPay Server's translation/localization code from the core server project into a dedicated 'Translations' plugin. The same dictionary-management features, language-pack download, and localization services are preserved, just relocated. There is no indication of a security fix or vulnerability being addressed.
No security action required. Treat as a normal refactoring commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change refactors translation-related classes (LocalizerService, LanguagePackUpdateService, Translations, view models, controllers, etc.) from BTCPayServer.Services/Controllers/Hosting into BTCPayServer.Plugins.Translations. UIServerController.Translations.cs is removed and replaced by a new UITranslationController in the plugin area. DI registrations move from BTCPayServerServices to the plugin. The diff is almost entirely namespace changes, file moves, and minor controller modernization (primary constructor, [Area], [AutoValidateAntiforgeryToken]). No security-relevant logic changes are visible.
Changed components
BTCPayServer.Plugins.TranslationsBTCPayServer/Controllers/UIServerControllerBTCPayServer/Hosting/BTCPayServerServicesBTCPayServer/Components/MainNav/Default.cshtmlInspect captured patch +3271 / −3292
diff --git a/BTCPayServer.Tests/LanguageServiceTests.cs b/BTCPayServer.Tests/LanguageServiceTests.cs
index df8e913..ac2e5bf 100644
--- a/BTCPayServer.Tests/LanguageServiceTests.cs
+++ b/BTCPayServer.Tests/LanguageServiceTests.cs
@@ -4,6 +4,7 @@ using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Data;
using BTCPayServer.Hosting;
+using BTCPayServer.Plugins.Translations;
using BTCPayServer.Services;
using Dapper;
using Microsoft.EntityFrameworkCore;
diff --git a/BTCPayServer.Tests/UtilitiesTests.cs b/BTCPayServer.Tests/UtilitiesTests.cs
index ed5b98b..5cb2f30 100644
--- a/BTCPayServer.Tests/UtilitiesTests.cs
+++ b/BTCPayServer.Tests/UtilitiesTests.cs
@@ -14,6 +14,7 @@ using Amazon.Runtime.Internal;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
+using BTCPayServer.Plugins.Translations;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
@@ -280,7 +281,7 @@ namespace BTCPayServer.Tests
obj.Add(v, "");
}
- var path = Path.Combine(soldir.FullName, "BTCPayServer/Services/Translations.Default.cs");
+ var path = Path.Combine(soldir.FullName, "BTCPayServer/Plugins/Translations/Translations.Default.cs");
var defaultTranslation = File.ReadAllText(path);
var startIdx = defaultTranslation.IndexOf("\"\"\"");
var endIdx = defaultTranslation.LastIndexOf("\"\"\"");
diff --git a/BTCPayServer/Components/MainNav/Default.cshtml b/BTCPayServer/Components/MainNav/Default.cshtml
index 4bd08ea..af90e55 100644
--- a/BTCPayServer/Components/MainNav/Default.cshtml
+++ b/BTCPayServer/Components/MainNav/Default.cshtml
@@ -8,6 +8,7 @@
@using BTCPayServer.Views.Apps
@using BTCPayServer.Configuration
@using BTCPayServer.Plugins.Emails
+@using BTCPayServer.Plugins.Translations
@inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContext;
@inject BTCPayServerOptions BtcPayServerOptions
@inject BTCPayServerEnvironment Env
@@ -323,7 +324,7 @@
<a layout-menu-item="@nameof(ServerNavPages.Branding)" asp-controller="UIServer" asp-action="Branding" text-translate="true">Branding</a>
</li>
<li class="nav-item nav-item-sub" permission="@Policies.CanModifyServerSettings">
- <a layout-menu-item="@nameof(ServerNavPages.Translations)" asp-controller="UIServer" asp-action="ListDictionaries" text-translate="true">Translations</a>
+ <a layout-menu-item="@nameof(ServerNavPages.Translations)" asp-area="@TranslationsPlugin.Area" asp-controller="UITranslation" asp-action="ListDictionaries" text-translate="true">Translations</a>
</li>
@if (BtcPayServerOptions.DockerDeployment)
{
diff --git a/BTCPayServer/Controllers/UIServerController.Translations.cs b/BTCPayServer/Controllers/UIServerController.Translations.cs
deleted file mode 100644
index aee99e1..0000000
--- a/BTCPayServer/Controllers/UIServerController.Translations.cs
+++ /dev/null
@@ -1,233 +0,0 @@
-using System;
-using System.Data.Common;
-using System.Linq;
-using System.Net.Http;
-using System.Threading.Tasks;
-using BTCPayServer.Abstractions.Constants;
-using BTCPayServer.Models.ServerViewModels;
-using BTCPayServer.Services;
-using Microsoft.AspNetCore.Mvc;
-using Newtonsoft.Json.Linq;
-
-namespace BTCPayServer.Controllers
-{
- public partial class UIServerController
- {
- [HttpGet("server/dictionaries")]
- public async Task<IActionResult> ListDictionaries()
- {
- var dictionaries = await _localizer.GetDictionaries();
- var vm = new ListDictionariesViewModel();
- var downloadableLanguages = LanguagePackUpdateService.GetDownloadableLanguages();
-
- foreach (var dictionary in dictionaries)
- {
- var isSelected = _policiesSettings.LangDictionary == dictionary.DictionaryName ||
- (_policiesSettings.LangDictionary is null && dictionary.Source == "Default");
- var isDownloadedPack = downloadableLanguages.Contains(dictionary.DictionaryName);
- var updateAvailable = false;
-
- if (isDownloadedPack && dictionary.Source == "Custom")
- {
- updateAvailable = await _languagePackUpdateService.CheckForLanguagePackUpdateCached(dictionary.DictionaryName, dictionary.Metadata);
- }
-
- var dict = new ListDictionariesViewModel.DictionaryViewModel
- {
- Editable = dictionary.Source == "Custom",
- Source = dictionary.Source,
- DictionaryName = dictionary.DictionaryName,
- Fallback = dictionary.Fallback,
- IsSelected = isSelected,
- IsDownloadedLanguagePack = isDownloadedPack && dictionary.Source == "Custom",
- UpdateAvailable = updateAvailable
- };
- if (isSelected)
- vm.Dictionaries.Insert(0, dict);
- else
- vm.Dictionaries.Add(dict);
- }
- return View(vm);
- }
-
- [HttpGet("server/dictionaries/create")]
- public async Task<IActionResult> CreateDictionary(string fallback = null)
- {
- var dictionaries = await _localizer.GetDictionaries();
- return View(new CreateDictionaryViewModel
- {
- Name = fallback is not null ? $"Clone of {fallback}" : "",
- Fallback = fallback ?? Translations.DefaultLanguage,
- }.SetDictionaries(dictionaries));
- }
-
- [HttpPost("server/dictionaries/create")]
- public async Task<IActionResult> CreateDictionary(CreateDictionaryViewModel viewModel)
- {
- if (ModelState.IsValid)
- {
- try
- {
- await _localizer.CreateDictionary(viewModel.Name, viewModel.Fallback, "Custom");
- }
- catch (DbException)
- {
- ModelState.AddModelError(nameof(viewModel.Name), StringLocalizer["'{0}' already exists", viewModel.Name]);
- }
- }
- if (!ModelState.IsValid)
- return View(viewModel.SetDictionaries(await _localizer.GetDictionaries()));
- TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dictionary created"].Value;
- return RedirectToAction(nameof(EditDictionary), new { dictionary = viewModel.Name });
- }
-
- [HttpGet("server/dictionaries/{dictionary}")]
- public async Task<IActionResult> EditDictionary(string dictionary)
- {
- if ((await _localizer.GetDictionary(dictionary)) is null)
- return NotFound();
- var translations = await _localizer.GetTranslations(dictionary);
- return View(new EditDictionaryViewModel().SetTranslations(translations.Translations));
- }
-
- [HttpPost("server/dictionaries/{dictionary}")]
- public async Task<IActionResult> EditDictionary(string dictionary, EditDictionaryViewModel viewModel)
- {
- var d = await _localizer.GetDictionary(dictionary);
- if (d is null)
- return NotFound();
- if (Environment.CheatMode && viewModel.Command == "Fake")
- {
- var t = await _localizer.GetTranslations(dictionary);
- var jobj = JObject.Parse(t.Translations.ToJsonFormat());
- foreach (var prop in jobj.Properties())
- {
- prop.Value = "OK";
- if (prop.Name.Contains("{0}")) prop.Value += " {0}";
- if (prop.Name.Contains("{1}")) prop.Value += " {1}";
- if (prop.Name.Contains("{2}")) prop.Value += " {2}";
- }
- viewModel.Translations = Translations.CreateFromJson(jobj.ToString()).ToJsonFormat();
- }
- if (!Translations.TryCreateFromJson(viewModel.Translations, out var translations))
- {
- ModelState.AddModelError(nameof(viewModel.Translations), StringLocalizer["Syntax error"]);
- return View(viewModel);
- }
- await _localizer.Save(d, translations);
- TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dictionary updated"].Value;
- return RedirectToAction(nameof(ListDictionaries));
- }
-
- [HttpGet("server/dictionaries/{dictionary}/select")]
- public async Task<IActionResult> SelectDictionary(string dictionary)
- {
- var settings = await _SettingsRepository.GetSettingAsync<PoliciesSettings>() ?? new();
- settings.LangDictionary = dictionary;
- await _SettingsRepository.UpdateSetting(settings);
- await _localizer.Load();
- TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Default dictionary changed to {0}", dictionary].Value;
- return RedirectToAction(nameof(ListDictionaries));
- }
-
- [HttpPost("server/dictionaries/{dictionary}/delete")]
- public async Task<IActionResult> DeleteDictionary(string dictionary)
- {
- await _localizer.DeleteDictionary(dictionary);
- TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dictionary {0} deleted", dictionary].Value;
- return RedirectToAction(nameof(ListDictionaries));
- }
-
- [HttpPost("server/dictionaries/download")]
- public async Task<IActionResult> DownloadLanguagePack(string language)
- {
- if (string.IsNullOrEmpty(language))
- {
- TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Please select a language"].Value;
- return RedirectToAction(nameof(ListDictionaries));
- }
-
- string translationsJson;
- string version;
- try
- {
- (translationsJson, version) = await FetchLanguagePackFromRepository(language);
- }
- catch (HttpRequestException ex)
- {
- TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Failed to download language pack: {0}", ex.Message].Value;
- return RedirectToAction(nameof(ListDictionaries));
- }
-
- var translations = Translations.CreateFromJson(translationsJson);
- var existingDictionary = await _localizer.GetDictionary(language);
- if (existingDictionary is null)
- {
- existingDictionary = await _localizer.CreateDictionary(language, Translations.DefaultLanguage, "Custom");
- TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Language pack '{0}' downloaded successfully", language].Value;
- }
- else
- {
- TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Language pack '{0}' updated successfully", language].Value;
- }
-
- await _localizer.Save(existingDictionary, translations);
- await _localizer.UpdateVersion(language, version);
- _languagePackUpdateService.InvalidateCache(language);
- return RedirectToAction(nameof(ListDictionaries));
- }
-
- [HttpPost("server/dictionaries/{dictionary}/update")]
- [ValidateAntiForgeryToken]
- public async Task<IActionResult> UpdateLanguagePack(string dictionary)
- {
- var existingDictionary = await _localizer.GetDictionary(dictionary);
- if (existingDictionary is null || !LanguagePackUpdateService.GetDownloadableLanguages().Contains(dictionary))
- {
- TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Dictionary not found or not a downloadable language pack"].Value;
- return RedirectToAction(nameof(ListDictionaries));
- }
-
- string translationsJson;
- string version;
- try
- {
- (translationsJson, version) = await FetchLanguagePackFromRepository(dictionary);
- }
- catch (HttpRequestException ex)
- {
- TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Failed to update language pack: {0}", ex.Message].Value;
- return RedirectToAction(nameof(ListDictionaries));
- }
-
- var translations = Translations.CreateFromJson(translationsJson);
- await _localizer.Save(existingDictionary, translations);
- await _localizer.UpdateVersion(dictionary, version);
- _languagePackUpdateService.InvalidateCache(dictionary);
- TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Language pack '{0}' updated successfully", dictionary].Value;
- return RedirectToAction(nameof(ListDictionaries));
- }
-
- private async Task<(string translationsJson, string version)> FetchLanguagePackFromRepository(string language)
- {
- if (!LanguagePackUpdateService.GetDownloadableLanguages().Contains(language))
- {
- throw new ArgumentException($"Language '{language}' is not a valid downloadable language pack.", nameof(language));
- }
-
- var fileName = Uri.EscapeDataString(language.ToLowerInvariant());
- var url = $"https://raw.githubusercontent.com/btcpayserver/btcpayserver-translator/main/translations/{fileName}.json";
-
- var httpClient = HttpClientFactory.CreateClient();
- httpClient.Timeout = TimeSpan.FromSeconds(30);
-
- var translationsJson = await httpClient.GetStringAsync(url);
-
- var hash = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(translationsJson));
- var version = Convert.ToHexString(hash);
-
- return (translationsJson, version);
- }
-
- }
-}
diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs
index 891ba86..e090fbd 100644
--- a/BTCPayServer/Controllers/UIServerController.cs
+++ b/BTCPayServer/Controllers/UIServerController.cs
@@ -1,7 +1,6 @@
#nullable enable
using System;
using System.Collections.Generic;
-using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
@@ -23,6 +22,7 @@ using BTCPayServer.Models.StoreViewModels;
using BTCPayServer.Services;
using BTCPayServer.Services.Apps;
using BTCPayServer.Plugins.Emails.Services;
+using BTCPayServer.Plugins.Translations;
using BTCPayServer.Services.Stores;
using BTCPayServer.Storage.Services;
using BTCPayServer.Storage.Services.Providers;
@@ -70,7 +70,6 @@ namespace BTCPayServer.Controllers
private readonly UriResolver _uriResolver;
private readonly TransactionLinkProviders _transactionLinkProviders;
private readonly LocalizerService _localizer;
- private readonly LanguagePackUpdateService _languagePackUpdateService;
private readonly EmailSenderFactory _emailSenderFactory;
public IStringLocalizer StringLocalizer { get; }
public ViewLocalizer ViewLocalizer { get; }
@@ -132,7 +131,6 @@ namespace BTCPayServer.Controllers
Html = html;
_transactionLinkProviders = transactionLinkProviders;
_localizer = localizer;
- _languagePackUpdateService = languagePackUpdateService;
Environment = environment;
StringLocalizer = stringLocalizer;
ViewLocalizer = viewLocalizer;
diff --git a/BTCPayServer/Extensions.cs b/BTCPayServer/Extensions.cs
index ff1135d..89642e7 100644
--- a/BTCPayServer/Extensions.cs
+++ b/BTCPayServer/Extensions.cs
@@ -45,7 +45,6 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
-using Microsoft.Extensions.Hosting;
using NBitcoin;
using NBitcoin.Payment;
using NBitcoin.RPC;
@@ -57,7 +56,7 @@ using InvoiceCryptoInfo = BTCPayServer.Services.Invoices.InvoiceCryptoInfo;
namespace BTCPayServer
{
- public static class Extensions
+ public static partial class Extensions
{
public static string GetNiceModelName(this HwiDeviceClient device)
=> device.Model switch
@@ -422,22 +421,6 @@ namespace BTCPayServer
}
}
-#nullable enable
- public static IServiceCollection AddDefaultTranslations(this IServiceCollection services, params string[] keyValues)
- {
- return services.AddDefaultTranslations(keyValues.Select(k => KeyValuePair.Create<string, string?>(k, string.Empty)).ToArray());
- }
- public static IServiceCollection AddDefaultPrettyName(this IServiceCollection services, PaymentMethodId paymentMethodId, string defaultPrettyName)
- {
- services.AddSingleton<PrettyNameProvider.UntranslatedPrettyName>(new PrettyNameProvider.UntranslatedPrettyName(paymentMethodId, defaultPrettyName));
- return services.AddDefaultTranslations(KeyValuePair.Create<string, string?>(PrettyNameProvider.GetTranslationKey(paymentMethodId), defaultPrettyName));
- }
- public static IServiceCollection AddDefaultTranslations(this IServiceCollection services, params KeyValuePair<string, string?>[] keyValues)
- {
- services.AddSingleton<IDefaultTranslationProvider>(new InMemoryDefaultTranslationProvider(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
diff --git a/BTCPayServer/Hosting/BTCPayServerServices.cs b/BTCPayServer/Hosting/BTCPayServerServices.cs
index b03cebd..86483dd 100644
--- a/BTCPayServer/Hosting/BTCPayServerServices.cs
+++ b/BTCPayServer/Hosting/BTCPayServerServices.cs
@@ -93,12 +93,6 @@ namespace BTCPayServer.Hosting
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration, Logs logs)
{
services.TryAddScoped<CallbackGenerator>();
- services.TryAddSingleton<IStringLocalizerFactory, LocalizerFactory>();
- services.TryAddSingleton<IHtmlLocalizerFactory, LocalizerFactory>();
- services.TryAddSingleton<LocalizerService>();
- services.TryAddSingleton<LanguagePackUpdateService>();
- services.TryAddSingleton<ViewLocalizer>();
- services.TryAddSingleton<IStringLocalizer>(o => o.GetRequiredService<IStringLocalizerFactory>().Create("", ""));
services.TryAddSingleton<DelayedTaskScheduler>();
services.TryAddSingleton<UIExtensionsRegistry>();
@@ -180,7 +174,6 @@ namespace BTCPayServer.Hosting
services.AddStartupTask<BlockExplorerLinkStartupTask>();
services.AddStartupTask<LoadCurrencyNameTableStartupTask>();
- services.AddStartupTask<LoadTranslationsStartupTask>();
services.TryAddSingleton<InvoiceRepository>();
services.AddSingleton<PaymentService>();
services.AddSingleton<BTCPayServerEnvironment>();
diff --git a/BTCPayServer/Hosting/LoadTranslationsStartupTask.cs b/BTCPayServer/Hosting/LoadTranslationsStartupTask.cs
deleted file mode 100644
index af660ef..0000000
--- a/BTCPayServer/Hosting/LoadTranslationsStartupTask.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System.IO;
-using System.Linq;
-using System.Security.Cryptography;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using BTCPayServer.Abstractions.Contracts;
-using BTCPayServer.Configuration;
-using BTCPayServer.Data;
-using BTCPayServer.Services;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using NBitcoin;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace BTCPayServer.Hosting
-{
- public class LoadTranslationsStartupTask : IStartupTask
- {
- public LoadTranslationsStartupTask(
- ILogger<LoadTranslationsStartupTask> logger,
- LocalizerService localizerService,
- IOptions<DataDirectories> dataDirectories)
- {
- DataDirectories = dataDirectories.Value;
- Logger = logger;
- LocalizerService = localizerService;
- }
-
- public DataDirectories DataDirectories { get; }
- public ILogger<LoadTranslationsStartupTask> Logger { get; }
- public LocalizerService LocalizerService { get; }
-
- class DictionaryFileMetadata
- {
- [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
- [JsonProperty("hash")]
- public uint256 Hash { get; set; }
- }
- public async Task ExecuteAsync(CancellationToken cancellationToken = default)
- {
- // This load languages files from a [datadir]/Langs into the database
- // to make startup faster, we skip update if we see that files didn't changes
- // since the last time they got loaded.
- // We do this by comparing hashes of the current file, to the one stored in DB.
- if (Directory.Exists(DataDirectories.LangsDir))
- {
- var files = Directory.GetFiles(DataDirectories.LangsDir);
- if (files.Length > 0)
- {
- Logger.LogInformation("Loading language files...");
- var dictionaries = await LocalizerService.GetDictionaries();
- foreach (var file in Directory.GetFiles(DataDirectories.LangsDir))
- {
- var langName = Path.GetFileName(file);
- var dictionary = dictionaries.FirstOrDefault(d => d.DictionaryName == langName);
- if (dictionary is null)
- dictionary = await LocalizerService.CreateDictionary(langName, null, "File");
- if (dictionary.Source != "File")
- {
- Logger.LogWarning($"Impossible to load language '{langName}', as it is already existing in the database, not initially imported by a File");
- continue;
- }
- var savedHash = dictionary.Metadata.ToObject<DictionaryFileMetadata>().Hash;
- var translations = Translations.CreateFromJson(File.ReadAllText(file));
- var currentHash = new uint256(SHA256.HashData(Encoding.UTF8.GetBytes(translations.ToJsonFormat())));
-
- if (savedHash != currentHash)
- {
- var newMetadata = (JObject)dictionary.Metadata.DeepClone();
- newMetadata["hash"] = currentHash.ToString();
- dictionary = dictionary with { Metadata = newMetadata };
- Logger.LogInformation($"Updating dictionary '{langName}'");
- await LocalizerService.Save(dictionary, translations);
- }
- }
- }
- }
-
- // Do not make startup longer for this
- _ = LocalizerService.Load();
- }
- }
-}
diff --git a/BTCPayServer/Models/ServerViewModels/CreateDictionaryViewModel.cs b/BTCPayServer/Models/ServerViewModels/CreateDictionaryViewModel.cs
deleted file mode 100644
index 058535a..0000000
--- a/BTCPayServer/Models/ServerViewModels/CreateDictionaryViewModel.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.Linq;
-using BTCPayServer.Services;
-using Microsoft.AspNetCore.Mvc.Rendering;
-
-namespace BTCPayServer.Models.ServerViewModels;
-public class CreateDictionaryViewModel
-{
- [Required(AllowEmptyStrings = false)]
- public string Name { get; set; }
- public string Fallback { get; set; }
- public SelectListItem[] DictionariesListItems { get; set; }
-
- internal CreateDictionaryViewModel SetDictionaries(LocalizerService.Dictionary[] dictionaries)
- {
- var items = dictionaries.Select(d => new SelectListItem(d.DictionaryName, d.DictionaryName, d.DictionaryName == Fallback)).ToArray();
- DictionariesListItems = items;
- return this;
- }
-}
diff --git a/BTCPayServer/Models/ServerViewModels/EditDictionaryViewModel.cs b/BTCPayServer/Models/ServerViewModels/EditDictionaryViewModel.cs
deleted file mode 100644
index 33f2bee..0000000
--- a/BTCPayServer/Models/ServerViewModels/EditDictionaryViewModel.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using BTCPayServer.Services;
-
-namespace BTCPayServer.Models.ServerViewModels;
-
-public class EditDictionaryViewModel
-{
- [Display(Name = "Translations")]
- public string Translations { get; set; }
- public int Lines { get; set; }
- public string Command { get; set; }
-
- internal EditDictionaryViewModel SetTranslations(Translations translations)
- {
- Translations = translations.ToJsonFormat();
- Lines = translations.Records.Count;
- return this;
- }
-}
diff --git a/BTCPayServer/Models/ServerViewModels/ListDictionariesViewModel.cs b/BTCPayServer/Models/ServerViewModels/ListDictionariesViewModel.cs
deleted file mode 100644
index 7c12d00..0000000
--- a/BTCPayServer/Models/ServerViewModels/ListDictionariesViewModel.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using System.Collections.Generic;
-
-namespace BTCPayServer.Models.ServerViewModels;
-
-public class ListDictionariesViewModel
-{
- public class DictionaryViewModel
- {
- public string DictionaryName { get; set; }
- public string Fallback { get; set; }
- public string Source { get; set; }
- public bool Editable { get; set; }
- public bool IsSelected { get; set; }
- public bool IsDownloadedLanguagePack { get; set; }
- public bool UpdateAvailable { get; set; }
- }
-
- public List<DictionaryViewModel> Dictionaries = [];
-}
diff --git a/BTCPayServer/Plugins/Translations/Controllers/UITranslationController.cs b/BTCPayServer/Plugins/Translations/Controllers/UITranslationController.cs
new file mode 100644
index 0000000..afa4768
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Controllers/UITranslationController.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Data.Common;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using BTCPayServer.Abstractions.Constants;
+using BTCPayServer.Models.ServerViewModels;
+using BTCPayServer.Plugins.Translations.Views;
+using BTCPayServer.Services;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Localization;
+using Newtonsoft.Json.Linq;
+
+namespace BTCPayServer.Plugins.Translations.Controllers;
+
+[Authorize(Policy = Client.Policies.CanModifyServerSettings,
+ AuthenticationSchemes = AuthenticationSchemes.Cookie)]
+[Area(TranslationsPlugin.Area)]
+[AutoValidateAntiforgeryToken]
+public class UITranslationController(
+ PoliciesSettings policiesSettings,
+ IStringLocalizer stringLocalizer,
+ LocalizerService localizer,
+ LanguagePackUpdateService languagePackUpdateService,
+ SettingsRepository settingsRepository,
+ BTCPayServerEnvironment environment) : Controller
+{
+ public IStringLocalizer StringLocalizer { get; } = stringLocalizer;
+
+ [HttpGet("server/dictionaries")]
+ public async Task<IActionResult> ListDictionaries()
+ {
+ var dictionaries = await localizer.GetDictionaries();
+ var vm = new ListDictionariesViewModel();
+ var downloadableLanguages = LanguagePackUpdateService.GetDownloadableLanguages();
+
+ foreach (var dictionary in dictionaries)
+ {
+ var isSelected = policiesSettings.LangDictionary == dictionary.DictionaryName ||
+ (policiesSettings.LangDictionary is null && dictionary.Source == "Default");
+ var isDownloadedPack = downloadableLanguages.Contains(dictionary.DictionaryName);
+ var updateAvailable = false;
+
+ if (isDownloadedPack && dictionary.Source == "Custom")
+ {
+ updateAvailable = await languagePackUpdateService.CheckForLanguagePackUpdateCached(dictionary.DictionaryName, dictionary.Metadata);
+ }
+
+ var dict = new ListDictionariesViewModel.DictionaryViewModel
+ {
+ Editable = dictionary.Source == "Custom",
+ Source = dictionary.Source,
+ DictionaryName = dictionary.DictionaryName,
+ Fallback = dictionary.Fallback,
+ IsSelected = isSelected,
+ IsDownloadedLanguagePack = isDownloadedPack && dictionary.Source == "Custom",
+ UpdateAvailable = updateAvailable
+ };
+ if (isSelected)
+ vm.Dictionaries.Insert(0, dict);
+ else
+ vm.Dictionaries.Add(dict);
+ }
+
+ return View(vm);
+ }
+
+ [HttpGet("server/dictionaries/create")]
+ public async Task<IActionResult> CreateDictionary(string fallback = null)
+ {
+ var dictionaries = await localizer.GetDictionaries();
+ return View(new CreateDictionaryViewModel
+ {
+ Name = fallback is not null ? $"Clone of {fallback}" : "",
+ Fallback = fallback ?? Translations.DefaultLanguage,
+ }.SetDictionaries(dictionaries));
+ }
+
+ [HttpPost("server/dictionaries/create")]
+ public async Task<IActionResult> CreateDictionary(CreateDictionaryViewModel viewModel)
+ {
+ if (ModelState.IsValid)
+ {
+ try
+ {
+ await localizer.CreateDictionary(viewModel.Name, viewModel.Fallback, "Custom");
+ }
+ catch (DbException)
+ {
+ ModelState.AddModelError(nameof(viewModel.Name), StringLocalizer["'{0}' already exists", viewModel.Name]);
+ }
+ }
+
+ if (!ModelState.IsValid)
+ return View(viewModel.SetDictionaries(await localizer.GetDictionaries()));
+ TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dictionary created"].Value;
+ return RedirectToAction(nameof(EditDictionary), new { dictionary = viewModel.Name });
+ }
+
+ [HttpGet("server/dictionaries/{dictionary}")]
+ public async Task<IActionResult> EditDictionary(string dictionary)
+ {
+ if ((await localizer.GetDictionary(dictionary)) is null)
+ return NotFound();
+ var translations = await localizer.GetTranslations(dictionary);
+ return View(new EditDictionaryViewModel().SetTranslations(translations.Translations));
+ }
+
+ [HttpPost("server/dictionaries/{dictionary}")]
+ public async Task<IActionResult> EditDictionary(string dictionary, EditDictionaryViewModel viewModel)
+ {
+ var d = await localizer.GetDictionary(dictionary);
+ if (d is null)
+ return NotFound();
+ if (environment.CheatMode && viewModel.Command == "Fake")
+ {
+ var t = await localizer.GetTranslations(dictionary);
+ var jobj = JObject.Parse(t.Translations.ToJsonFormat());
+ foreach (var prop in jobj.Properties())
+ {
+ prop.Value = "OK";
+ if (prop.Name.Contains("{0}")) prop.Value += " {0}";
+ if (prop.Name.Contains("{1}")) prop.Value += " {1}";
+ if (prop.Name.Contains("{2}")) prop.Value += " {2}";
+ }
+
+ viewModel.Translations = Translations.CreateFromJson(jobj.ToString()).ToJsonFormat();
+ }
+
+ if (!Translations.TryCreateFromJson(viewModel.Translations, out var translations))
+ {
+ ModelState.AddModelError(nameof(viewModel.Translations), StringLocalizer["Syntax error"]);
+ return View(viewModel);
+ }
+
+ await localizer.Save(d, translations);
+ TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dictionary updated"].Value;
+ return RedirectToAction(nameof(ListDictionaries));
+ }
+
+ [HttpPost("server/dictionaries/{dictionary}/select")]
+ public async Task<IActionResult> SelectDictionary(string dictionary)
+ {
+ if ((await localizer.GetDictionary(dictionary)) is null)
+ return NotFound();
+ var settings = await settingsRepository.GetSettingAsync<PoliciesSettings>() ?? new();
+ settings.LangDictionary = dictionary;
+ await settingsRepository.UpdateSetting(settings);
+ await localizer.Load();
+ TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Default dictionary changed to {0}", dictionary].Value;
+ return RedirectToAction(nameof(ListDictionaries));
+ }
+
+ [HttpPost("server/dictionaries/{dictionary}/delete")]
+ public async Task<IActionResult> DeleteDictionary(string dictionary)
+ {
+ await localizer.DeleteDictionary(dictionary);
+ TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Dictionary {0} deleted", dictionary].Value;
+ return RedirectToAction(nameof(ListDictionaries));
+ }
+
+ [HttpPost("server/dictionaries/download")]
+ public async Task<IActionResult> DownloadLanguagePack(string language)
+ {
+ if (string.IsNullOrEmpty(language))
+ {
+ TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Please select a language"].Value;
+ return RedirectToAction(nameof(ListDictionaries));
+ }
+
+ string translationsJson;
+ string version;
+ try
+ {
+ (translationsJson, version) = await languagePackUpdateService.FetchLanguagePackFromRepository(language);
+ }
+ catch (Exception ex)
+ {
+ TempData[WellKnownTempData.ErrorMessage] = StringLocalizer["Failed to download language pack: {0}", ex.Message].Value;
+ return RedirectToAction(nameof(ListDictionaries));
+ }
+
+ var translations = Translations.CreateFromJson(translationsJson);
+ var existingDictionary = await localizer.GetDictionary(language);
+ if (existingDictionary is null)
+ {
+ existingDictionary = await localizer.CreateDictionary(language, Translations.DefaultLanguage, "Custom");
+ TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Language pack '{0}' downloaded successfully", language].Value;
+ }
+ else
+ {
+ TempData[WellKnownTempData.SuccessMessage] = StringLocalizer["Language pack '{0}' updated successfully", language].Value;
+ }
+
+ await localizer.Save(existingDictionary, translations);
+ await localizer.UpdateVersion(language, version);
+ languagePackUpdateService.InvalidateCache(language);
+ return RedirectToAction(nameof(ListDictionaries));
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/IDefaultTranslationProvider.cs b/BTCPayServer/Plugins/Translations/IDefaultTranslationProvider.cs
new file mode 100644
index 0000000..7a35144
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/IDefaultTranslationProvider.cs
@@ -0,0 +1,11 @@
+// We don't want to break plugins, so let's not fix the namespace.
+
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace BTCPayServer.Services;
+
+public interface IDefaultTranslationProvider
+{
+ Task<KeyValuePair<string, string>[]> GetDefaultTranslations();
+}
diff --git a/BTCPayServer/Plugins/Translations/LanguagePackUpdateService.cs b/BTCPayServer/Plugins/Translations/LanguagePackUpdateService.cs
new file mode 100644
index 0000000..a03cd55
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/LanguagePackUpdateService.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Concurrent;
+using System.Linq;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Newtonsoft.Json.Linq;
+
+namespace BTCPayServer.Plugins.Translations
+{
+ public class LanguagePackUpdateService(IHttpClientFactory httpClientFactory)
+ {
+ private readonly ConcurrentDictionary<string, (bool UpdateAvailable, DateTime CheckedAt)> _updateCheckCache = new();
+ private readonly TimeSpan _cacheExpiration = TimeSpan.FromHours(1);
+
+ public static string[] GetDownloadableLanguages()
+ {
+ return new[]
+ {
+ "Dutch",
+ "French",
+ "German",
+ "Hindi",
+ "Indonesian",
+ "Italian",
+ "Japanese",
+ "Norwegian",
+ "Korean",
+ "Portuguese (Brazil)",
+ "Russian",
+ "Serbian",
+ "Spanish",
+ "Thai",
+ "Turkish"
+ };
+ }
+
+ public async Task<(string translationsJson, string version)> FetchLanguagePackFromRepository(string language)
+ {
+ if (!GetDownloadableLanguages().Contains(language))
+ {
+ throw new ArgumentException($"Language '{language}' is not a valid downloadable language pack.", nameof(language));
+ }
+
+ var fileName = Uri.EscapeDataString(language.ToLowerInvariant());
+ var url = $"https://raw.githubusercontent.com/btcpayserver/btcpayserver-translator/main/translations/{fileName}.json";
+
+ var httpClient = httpClientFactory.CreateClient();
+ httpClient.Timeout = TimeSpan.FromSeconds(30);
+
+ var translationsJson = await httpClient.GetStringAsync(url);
+
+ var hash = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(translationsJson));
+ var version = Convert.ToHexString(hash);
+
+ return (translationsJson, version);
+ }
+
+ public async Task<bool> CheckForLanguagePackUpdateCached(string language, JObject metadata)
+ {
+ var cacheKey = language;
+
+ if (_updateCheckCache.TryGetValue(cacheKey, out var cached))
+ {
+ if (DateTime.UtcNow - cached.CheckedAt < _cacheExpiration)
+ {
+ return cached.UpdateAvailable;
+ }
+ }
+
+ var updateAvailable = await CheckForLanguagePackUpdate(language, metadata);
+ _updateCheckCache[cacheKey] = (updateAvailable, DateTime.UtcNow);
+
+ return updateAvailable;
+ }
+
+ public void InvalidateCache(string language)
+ {
+ _updateCheckCache.TryRemove(language, out _);
+ }
+
+ private async Task<bool> CheckForLanguagePackUpdate(string language, JObject metadata)
+ {
+ try
+ {
+ if (!GetDownloadableLanguages().Contains(language))
+ {
+ return false;
+ }
+
+
+ var (_, remoteVersion) = await FetchLanguagePackFromRepository(language);
+ var localVersion = metadata["version"]?.ToString();
+
+ if (string.IsNullOrEmpty(localVersion))
+ return true;
+
+ return remoteVersion != localVersion;
+ }
+ catch (HttpRequestException)
+ {
+ return false;
+ }
+ catch (TaskCanceledException)
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/LoadTranslationsStartupTask.cs b/BTCPayServer/Plugins/Translations/LoadTranslationsStartupTask.cs
new file mode 100644
index 0000000..4f86f82
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/LoadTranslationsStartupTask.cs
@@ -0,0 +1,77 @@
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using BTCPayServer.Abstractions.Contracts;
+using BTCPayServer.Configuration;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using NBitcoin;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace BTCPayServer.Plugins.Translations
+{
+ public class LoadTranslationsStartupTask(
+ ILogger<LoadTranslationsStartupTask> logger,
+ LocalizerService localizerService,
+ IOptions<DataDirectories> dataDirectories)
+ : IStartupTask
+ {
+ public DataDirectories DataDirectories { get; } = dataDirectories.Value;
+ public ILogger<LoadTranslationsStartupTask> Logger { get; } = logger;
+ public LocalizerService LocalizerService { get; } = localizerService;
+
+ class DictionaryFileMetadata
+ {
+ [JsonConverter(typeof(NBitcoin.JsonConverters.UInt256JsonConverter))]
+ [JsonProperty("hash")]
+ public uint256 Hash { get; set; }
+ }
+ public async Task ExecuteAsync(CancellationToken cancellationToken = default)
+ {
+ // This load languages files from a [datadir]/Langs into the database
+ // to make startup faster, we skip update if we see that files didn't changes
+ // since the last time they got loaded.
+ // We do this by comparing hashes of the current file, to the one stored in DB.
+ if (Directory.Exists(DataDirectories.LangsDir))
+ {
+ var files = Directory.GetFiles(DataDirectories.LangsDir);
+ if (files.Length > 0)
+ {
+ Logger.LogInformation("Loading language files...");
+ var dictionaries = await LocalizerService.GetDictionaries();
+ foreach (var file in files)
+ {
+ var langName = Path.GetFileName(file);
+ var dictionary = dictionaries.FirstOrDefault(d => d.DictionaryName == langName);
+ if (dictionary is null)
+ dictionary = await LocalizerService.CreateDictionary(langName, null, "File");
+ if (dictionary.Source != "File")
+ {
+ Logger.LogWarning($"Impossible to load language '{langName}', as it is already existing in the database, not initially imported by a File");
+ continue;
+ }
+ var savedHash = dictionary.Metadata.ToObject<DictionaryFileMetadata>().Hash;
+ var translations = Translations.CreateFromJson(await File.ReadAllTextAsync(file, cancellationToken));
+ var currentHash = new uint256(SHA256.HashData(Encoding.UTF8.GetBytes(translations.ToJsonFormat())));
+
+ if (savedHash != currentHash)
+ {
+ var newMetadata = (JObject)dictionary.Metadata.DeepClone();
+ newMetadata["hash"] = currentHash.ToString();
+ dictionary = dictionary with { Metadata = newMetadata };
+ Logger.LogInformation($"Updating dictionary '{langName}'");
+ await LocalizerService.Save(dictionary, translations);
+ }
+ }
+ }
+ }
+
+ // Do not make startup longer for this
+ _ = LocalizerService.Load();
+ }
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/LocalizerFactory.cs b/BTCPayServer/Plugins/Translations/LocalizerFactory.cs
new file mode 100644
index 0000000..ea3b0b7
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/LocalizerFactory.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using BTCPayServer.Logging;
+using Microsoft.AspNetCore.Mvc.Localization;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Logging;
+
+namespace BTCPayServer.Plugins.Translations
+{
+ public class LocalizerFactory : IStringLocalizerFactory, IHtmlLocalizerFactory
+ {
+ internal readonly Logs _logs;
+ private readonly LocalizerService _localizerService;
+
+ class StringLocalizer : IStringLocalizer, IHtmlLocalizer
+ {
+ private Type _resourceSource;
+ private string _baseName;
+ private string _location;
+ private LocalizerFactory _Factory;
+
+ public StringLocalizer(LocalizerFactory factory, Type resourceSource)
+ {
+ _Factory = factory;
+ _resourceSource = resourceSource;
+ }
+ Logs Logs => _Factory._logs;
+
+
+ public StringLocalizer(LocalizerFactory jsonStringLocalizerFactory, string baseName, string location)
+ {
+ _Factory = jsonStringLocalizerFactory;
+ _baseName = baseName;
+ _location = location;
+ }
+ Translations Translations => _Factory._localizerService.Translations;
+ public LocalizedString this[string name]
+ {
+ get
+ {
+ //Logs.PayServer.LogInformation($"this[name] with name:{name}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
+ Translations.Records.TryGetValue(name, out var result);
+ result = result ?? name;
+ return new LocalizedString(name, result);
+ }
+ }
+
+ public LocalizedString this[string name, params object[] arguments]
+ {
+ get
+ {
+ //var args = String.Join(", ", arguments.Select((a, i) => $"arg[{i}]:{a}").ToArray());
+ //Logs.PayServer.LogInformation($"this[name, arguments] with name:{name}, {args}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
+ Translations.Records.TryGetValue(name, out var result);
+ result = result ?? name;
+ return new LocalizedString(name, string.Format(result, arguments));
+ }
+ }
+
+ public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
+ {
+ //Logs.PayServer.LogInformation($"GetAllStrings");
+ return Translations.Records.Select(r => new LocalizedString(r.Key, r.Value));
+ }
+
+ LocalizedHtmlString IHtmlLocalizer.this[string name]
+ {
+ get
+ {
+ //Logs.PayServer.LogInformation($"[HTML]: this[name] with name:{name}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
+ Translations.Records.TryGetValue(name, out var result);
+ result = result ?? name;
+ return new LocalizedHtmlString(name, result);
+ }
+ }
+
+ LocalizedHtmlString IHtmlLocalizer.this[string name, params object[] arguments]
+ {
+ get
+ {
+ //var args = String.Join(", ", arguments.Select((a, i) => $"arg[{i}]:{a}").ToArray());
+ //Logs.PayServer.LogInformation($"[HTML]:this[name, arguments] with name:{name}, {args}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
+ Translations.Records.TryGetValue(name, out var result);
+ result = result ?? name;
+ return new LocalizedHtmlString(name, result, true, arguments);
+ }
+ }
+
+ public LocalizedString GetString(string name)
+ {
+ //Logs.PayServer.LogInformation($"[HTML] GetString(name):");
+ return this[name];
+ }
+
+ public LocalizedString GetString(string name, params object[] arguments)
+ {
+ //var args = String.Join(", ", arguments.Select((a, i) => $"arg[{i}]:{a}").ToArray());
+ Logs.PayServer.LogInformation($"[HTML] GetString(name,args):");
+ return this[name, arguments];
+ }
+ }
+ public LocalizerFactory(Logs logs, LocalizerService localizerService)
+ {
+ _logs = logs;
+ _localizerService = localizerService;
+ }
+ public IStringLocalizer Create(Type resourceSource)
+ {
+ return new StringLocalizer(this, resourceSource);
+ }
+
+ public IStringLocalizer Create(string baseName, string location)
+ {
+ return new StringLocalizer(this, baseName, location);
+ }
+
+ IHtmlLocalizer IHtmlLocalizerFactory.Create(Type resourceSource)
+ {
+ return new StringLocalizer(this, resourceSource);
+ }
+
+ IHtmlLocalizer IHtmlLocalizerFactory.Create(string baseName, string location)
+ {
+ return new StringLocalizer(this, baseName, location);
+ }
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/LocalizerService.cs b/BTCPayServer/Plugins/Translations/LocalizerService.cs
new file mode 100644
index 0000000..87c66db
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/LocalizerService.cs
@@ -0,0 +1,178 @@
+#nullable enable
+using Dapper;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using BTCPayServer.Data;
+using Microsoft.EntityFrameworkCore;
+using System;
+using BTCPayServer.Services;
+using Newtonsoft.Json.Linq;
+using Microsoft.Extensions.Logging;
+
+namespace BTCPayServer.Plugins.Translations
+{
+ public class InMemoryDefaultTranslationProvider(KeyValuePair<string, string?>[] values) : IDefaultTranslationProvider
+ {
+ public Task<KeyValuePair<string, string?>[]> GetDefaultTranslations()
+ {
+ return Task.FromResult(values);
+ }
+ }
+ public class LocalizerService(
+ ILogger<LocalizerService> logger,
+ ApplicationDbContextFactory contextFactory,
+ ISettingsAccessor<PoliciesSettings> settingsAccessor,
+ IEnumerable<IDefaultTranslationProvider> defaultTranslationProviders)
+ {
+ public record LoadedTranslations(Translations Translations, Translations Fallback, string LangName);
+ LoadedTranslations _LoadedTranslations = new(Translations.Default, Translations.Default, Translations.DefaultLanguage);
+ public Translations Translations => _LoadedTranslations.Translations;
+
+ /// <summary>
+ /// Load the translation of the server into memory
+ /// </summary>
+ /// <returns></returns>
+ public async Task Load()
+ {
+ try
+ {
+ _LoadedTranslations = await GetTranslations(settingsAccessor.Settings.LangDictionary);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to load translations");
+ throw;
+ }
+ }
+
+ public async Task<LoadedTranslations> GetTranslations(string dictionaryName)
+ {
+ await using var ctx = contextFactory.CreateContext();
+ var conn = ctx.Database.GetDbConnection();
+ var all = await conn.QueryAsync<(bool fallback, string sentence, string? translation)>(
+ "SELECT 'f'::BOOL fallback, sentence, translation FROM translations WHERE dict_id=@dict_id " +
+ "UNION ALL " +
+ "SELECT 't'::BOOL fallback, sentence, translation FROM translations WHERE dict_id=(SELECT fallback FROM lang_dictionaries WHERE dict_id=@dict_id)",
+ new
+ {
+ dict_id = dictionaryName,
+ });
+ var defaultDict = Translations.Default;
+ var loading = defaultTranslationProviders.Select(d => d.GetDefaultTranslations()).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);
+ }
+
+ public async Task Save(Dictionary dictionary, Translations translations)
+ {
+ var loadedTranslations = await GetTranslations(dictionary.DictionaryName);
+ translations = translations.WithFallback(loadedTranslations.Fallback);
+ await using var ctx = contextFactory.CreateContext();
+ var diffs = loadedTranslations.Translations.CalculateDiff(translations);
+ var conn = ctx.Database.GetDbConnection();
+ List<string> keys = new List<string>();
+ List<string> deletedKeys = new List<string>();
+ List<string> values = new List<string>();
+
+ // The basic idea here is that we can remove from
+ // the dictionary any translations which are the same
+ // as the fallback. This way, if the fallback gets updated,
+ // it will also update the dictionary.
+ foreach (var diff in diffs)
+ {
+ if (diff is Translations.Diff.Added a)
+ {
+ if (a.Value != loadedTranslations.Fallback[a.Key])
+ {
+ keys.Add(a.Key);
+ values.Add(a.Value);
+ }
+ }
+ else if (diff is Translations.Diff.Modified m)
+ {
+ if (m.NewValue != loadedTranslations.Fallback[m.Key])
+ {
+ keys.Add(m.Key);
+ values.Add(m.NewValue);
+ }
+ else
+ {
+ deletedKeys.Add(m.Key);
+ }
+ }
+ else if (diff is Translations.Diff.Deleted d)
+ {
+ deletedKeys.Add(d.Key);
+ }
+ }
+ await conn.ExecuteAsync("INSERT INTO lang_translations SELECT @dict_id, sentence, translation FROM unnest(@keys, @values) AS t(sentence, translation) ON CONFLICT (dict_id, sentence) DO UPDATE SET translation = EXCLUDED.translation; ",
+ new
+ {
+ dict_id = loadedTranslations.LangName,
+ keys = keys.ToArray(),
+ values = values.ToArray()
+ });
+ await conn.ExecuteAsync("DELETE FROM lang_translations WHERE dict_id=@dict_id AND sentence=ANY(@keys)",
+ new
+ {
+ dict_id = loadedTranslations.LangName,
+ keys = deletedKeys.ToArray()
+ });
+
+ if (_LoadedTranslations.LangName == loadedTranslations.LangName)
+ _LoadedTranslations = loadedTranslations with { Translations = translations };
+ }
+
+ public record Dictionary(string DictionaryName, string? Fallback, string Source, JObject Metadata);
+ public async Task<Dictionary[]> GetDictionaries()
+ {
+ await using var ctx = contextFactory.CreateContext();
+ var db = ctx.Database.GetDbConnection();
+ var rows = await db.QueryAsync<(string dict_id, string? fallback, string? source, string? metadata)>("SELECT * FROM lang_dictionaries");
+ return rows.Select(r => new Dictionary(r.dict_id, r.fallback, r.source ?? "", JObject.Parse(r.metadata ?? "{}"))).ToArray();
+ }
+ public async Task<Dictionary?> GetDictionary(string name)
+ {
+ await using var ctx = contextFactory.CreateContext();
+ var db = ctx.Database.GetDbConnection();
+ var r = await db.QueryFirstOrDefaultAsync("SELECT * FROM lang_dictionaries WHERE dict_id=@dict_id", new { dict_id = name });
+ if (r is null)
+ return null;
+ return new Dictionary(r.dict_id, r.fallback, r.source ?? "", JObject.Parse(r.metadata ?? "{}"));
+ }
+
+ public async Task<Dictionary> CreateDictionary(string langName, string? fallback, string source)
+ {
+ await using var ctx = contextFactory.CreateContext();
+ var db = ctx.Database.GetDbConnection();
+ await db.ExecuteAsync("INSERT INTO lang_dictionaries (dict_id, fallback, source) VALUES (@langName, @fallback, @source)", new { langName, fallback, source });
+ return new Dictionary(langName, fallback, source ?? "", new JObject());
+ }
+
+ public async Task DeleteDictionary(string dictionary)
+ {
+ await using var ctx = contextFactory.CreateContext();
+ var db = ctx.Database.GetDbConnection();
+ await db.ExecuteAsync("DELETE FROM lang_dictionaries WHERE dict_id=@dict_id AND source='Custom'", new { dict_id = dictionary });
+ }
+
+ public async Task UpdateVersion(string dictionary, string version)
+ {
+ await using var ctx = contextFactory.CreateContext();
+ var db = ctx.Database.GetDbConnection();
+ await db.ExecuteAsync("UPDATE lang_dictionaries SET metadata = jsonb_set(COALESCE(metadata, '{}'::jsonb), '{version}', to_jsonb(@version::text)) WHERE dict_id = @dict_id",
+ new { dict_id = dictionary, version });
+ }
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/Translations.Default.cs b/BTCPayServer/Plugins/Translations/Translations.Default.cs
new file mode 100644
index 0000000..58230d0
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Translations.Default.cs
@@ -0,0 +1,2112 @@
+using System.Collections.Generic;
+
+namespace BTCPayServer.Plugins.Translations
+{
+ public partial class Translations
+ {
+ static Translations()
+ {
+ // Text generated by UpdateDefaultTranslations.
+ // Please run it before release.
+ var knownTranslations =
+"""
+{
+ "... on every payment": "",
+ "... only if the customer makes more than one payment for the invoice": "",
+ "'{0}' already exists": "",
+ "'Anyone can invoice' is turned off": "",
+ "({0} migrated users)": "",
+ "{0} Archived Store": "",
+ "{0} Archived Stores": "",
+ "{0} day": "",
+ "{0} days": "",
+ "{0} files were added. {1} files had invalid names": "",
+ "{0} for {1} or {2}": "",
+ "{0} invoice archived.": "",
+ "{0} invoice unarchived.": "",
+ "{0} invoices archived.": "",
+ "{0} invoices unarchived.": "",
+ "{0} is not fully synched": "",
+ "{0} left": "",
+ "{0} Lightning": "",
+ "{0} Lightning Node": "",
+ "{0} Lightning node updated.": "",
+ "{0} Lightning Settings": "",
+ "{0} Lightning settings successfully updated.": "",
+ "{0} minutes": "",
+ "{0} Node": "",
+ "{0} provider is not supported": "",
+ "{0} selected": "",
+ "{0} Status": "",
+ "{0} Store": "",
+ "{0} Stores": "",
+ "{0} total": "",
+ "{0} Transactions": "",
+ "{0} users migrated to the plan '{1}'.": "",
+ "{0} wallet": "",
+ "{0} Wallet": "",
+ "{0} Wallet Labels": "",
+ "{0} Wallet Settings": "",
+ "@submitLabel": "",
+ "<code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps": "",
+ "<code>orderid:id</code> for filtering a specific order": "",
+ "<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": "",
+ "<strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:": "",
+ "1 day": "",
+ "24 Hours": "",
+ "2FA and U2F/FIDO2 and LNURL-Auth Authentication Methods are not available. Please go to the https endpoint.": "",
+ "3 Days": "",
+ "30 days": "",
+ "7 days": "",
+ "7 Days": "",
+ "A \"Contact Us\" button with this link will be shown on the checkout page. Can contain the placeholders <code>{OrderId}</code> and <code>{InvoiceId}</code>. Can be any valid URI, such as a website, email, and Nostr.": "",
+ "A <code>POST</code> callback will be sent to the specified <code>notificationUrl</code> (for on-chain transactions when there are sufficient confirmations):": "",
+ "A camera was not detected on your device.": "",
+ "A delay means your service either failed to process earlier deliveries or is taking too long to respond.": "",
+ "A given currency pair match the most specific rule. If two rules are matching and are as specific, the first\n rule\n will be chosen.": "",
+ "A malicious actor with access to this QR Code can affect the performances of your server.": "",
+ "A malicious actor with access to this QR Code could affect the performances of your server and might steal your funds.": "",
+ "A malicious actor with access to this QR Code could steal the funds on your lightning wallet.": "",
+ "A new payout is approved and awaiting payment": "",
+ "A new payout is awaiting for approval": "",
+ "A payment request with reference ID \"{0}\" already exists for this store.": "",
+ "A payment that was made to an approved payout by an external wallet is waiting for your confirmation.": "",
+ "A permission to the camera is needed to scan the QR code. Please grant the browser access and then retry.": "",
+ "A Postgres compatible JSON Path (eg. $ ? (@.Customer.Name == \"John\"))": "",
+ "A self-hosted, open-source bitcoin payment processor.": "",
+ "Access to vault granted by owner.": "",
+ "Access Tokens": "",
+ "Access Type": "",
+ "Account": "",
+ "Account created.": "",
+ "Account key": "",
+ "Account Key": "",
+ "Account key path": "",
+ "Account successfully created.": "",
+ "Account successfully deleted.": "",
+ "Action canceled by user": "",
+ "Actions": "",
+ "Activate Monetization": "",
+ "Active": "",
+ "Active Members": "",
+ "Active Subscribers": "",
+ "Add": "",
+ "Add additional fee (network fee) to invoice …": "",
+ "Add Address": "",
+ "Add an email address or an external URL where users can contact you for support requests through a \"Contact Us\" button, displayed at the bottom of the public facing pages.": "",
+ "Add destination": "",
+ "Add domain mapping": "",
+ "Add Exchange Rate Spread": "",
+ "Add Form Field": "",
+ "Add hop hints for private channels to the Lightning invoice": "",
+ "Add Item": "",
+ "Add mapped value": "",
+ "Add Option": "",
+ "Add or customize translations": "",
+ "Add plan": "",
+ "Add Plan": "",
+ "Add plugin manually": "",
+ "Add Role": "",
+ "Add Service": "",
+ "Add subscriber": "",
+ "Add User": "",
+ "Add Webhook": "",
+ "Additional Actions": "",
+ "Additional Options": "",
+ "Additional rates to track": "",
+ "Additional text to provide an explanation for the field": "",
+ "Address": "",
+ "Address type": "",
+ "Address verified.": "",
+ "Addresses": "",
+ "Adjust the design of your BTCPay Server instance to your needs.": "",
+ "Adjusts the generated invoice amount — use as a prefix to have multiple adjustment fields": "",
+ "Adjusts the generated invoice amount by multiplying with this value — use as a prefix to have multiple adjustment fields": "",
+ "Admin API access token": "",
+ "Admin must approve new users": "",
+ "Administrator": "",
+ "Advanced Options": "",
+ "Advanced rate rule scripting": "",
+ "Advanced settings": "",
+ "All": "",
+ "All invoice updates": "",
+ "All Labels": "",
+ "All notifications are disabled.": "",
+ "All Plugins": "",
+ "All Status": "",
+ "All Stores": "",
+ "All Time": "",
+ "All Type": "",
+ "Allow anyone to create invoice": "",
+ "Allow form for public use": "",
+ "Allow payee to create invoices with custom amounts": "",
+ "Allow payee to pass a comment": "",
+ "Allow Stores use the Server's SMTP email settings as their default": "",
+ "Already claimed": "",
+ "Alternatively, you can use the invoice API by including the following HTTP Header in your requests:": "",
+ "Alternatives": "",
+ "Always include non-witness UTXO if available": "",
+ "Amazon S3": "",
+ "Amazon S3 Storage": "",
+ "Amount": "",
+ "Amount and currency are not editable once payment request has invoices": "",
+ "Amount must be greater than 0": "",
+ "Amount requested": "",
+ "An error occurred while resetting user password": "",
+ "An error occurred while saving: {0}": "",
+ "An invitation email has been sent.<br/>You may alternatively share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "",
+ "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "",
+ "An invoice must be paid within a defined time interval at a fixed exchange rate to protect the issuer from price fluctuations.": "",
+ "An unexpected error happened: {0}": "",
+ "Animation": "",
+ "Any amount": "",
+ "Any application using the API key will immediately lose access.": "",
+ "Any uploaded files are being saved on the same machine that hosts BTCPay; please pay attention to your storage space.": "",
+ "API": "",
+ "API authentication docs": "",
+ "API Key": "",
+ "API key generated!": "",
+ "API Key removed": "",
+ "API Keys": "",
+ "App": "",
+ "App deleted successfully.": "",
+ "App Item/Perk": "",
+ "App Name": "",
+ "App not found": "",
+ "App successfully created": "",
+ "App Type": "",
+ "App updated": "",
+ "App-based 2FA": "",
+ "Application": "",
+ "Apply the brand color to the store's backend as well": "",
+ "Approve": "",
+ "Approve & Send": "",
+ "Approve this pairing demand": "",
+ "Approve user": "",
+ "Approved": "",
+ "Archive": "",
+ "Archive pull payment": "",
+ "Archive this app": "",
+ "Archive this app so that it does not appear in the apps list by default": "",
+ "Archive this invoice so that it does not appear in the invoice list by default": "",
+ "Archive this payment request so that it does not appear in the payment request list by default": "",
+ "Archive this store": "",
+ "Archive this store so that it does not appear in the stores list by default": "",
+ "archived": "",
+ "Archived Stores": "",
+ "Authenticator code": "",
+ "Authorize a public key to access Bitpay compatible Invoice API.": "",
+ "Authorize app": "",
+ "Authorized keys": "",
+ "authorized_keys has been updated": "",
+ "Auto-detect language on checkout": "",
+ "Automated Bitcoin Sender": "",
+ "Automated Lightning Sender": "",
+ "Automatic redelivery": "",
+ "Automatically approve claims": "",
+ "Automatically Approved": "",
+ "available": "",
+ "Available claim": "",
+ "Available Filters (click to expand)": "",
+ "Available Payment Methods": "",
+ "Available placeholders: <code>{StoreName} {ItemDescription} {OrderId}</code>": "",
+ "Available Plugins": "",
+ "Awaiting": "",
+ "Azure Blob Storage": "",
+ "Backend's language": "",
+ "Balance": "",
+ "Base URL": "",
+ "Batch size": "",
+ "BCC": "",
+ "Before being able to upload you first need to {0}.": "",
+ "Before you proceed, please understand the following:": "",
+ "BIP39 Seed (12/24 word mnemonic phrase) or HD private key (xprv...)": "",
+ "Block Explorers": "",
+ "blocks": "",
+ "Boltcard is configured": "",
+ "Boltcard URL": "",
+ "Brand Color": "",
+ "Branding": "",
+ "Broadcast (Payjoin)": "",
+ "Broadcast (Simple)": "",
+ "Broadcast transaction": "",
+ "Browser connection": "",
+ "Browser Redirect": "",
+ "BTCPay exposes Core Lightning's REST service for outside consumption, you will find connection information here.": "",
+ "BTCPay is expecting you to access this website from <strong>": "",
+ "BTCPay Server Configurator": "",
+ "BTCPay Server currently supports:": "",
+ "BTCPay Server Registration will redirect to a custom registration page": "",
+ "BTCPay Server Supporters": "",
+ "BTCPay will restart momentarily.": "",
+ "Bump fee": "",
+ "But now, what if you want to support <code>DOGE</code>? The problem with <code>DOGE</code> is that most\n exchange do\n not have any pair for it. But <code>bitpay</code> has a <code>DOGE_BTC</code> pair.<br />Luckily, the rule engine allow you to\n reference\n rules:": "",
+ "Button Type": "",
+ "Buy Button Text": "",
+ "Buyer Email": "",
+ "By confirming, you will deactivate the monetization feature, user access will not be dependent on subscriptions anymore.": "",
+ "By proceeding, all non-admin users will be migrated to the selected plan. If the plan does not include the <b>can-access</b> feature, the user accounts will be disabled.": "",
+ "Callback Notification URL": "",
+ "Campaign not active": "",
+ "Can access BTCPay Server": "",
+ "Can create a new cold wallet": "",
+ "Can use hot wallet": "",
+ "Can use RPC import": "",
+ "Cancel": "",
+ "Cancel Invoice": "",
+ "Cannot delete plan. It is currently in use by subscribers.": "",
+ "Cannot generate API keys while not using HTTPS or Tor": "",
+ "Card reset succeed": "",
+ "Categories": "",
+ "Caution: Allowing non-admins to have access to API endpoints may expose your BTCPay Server instance to potential security risks from unknown users.": "",
+ "Caution: Enabling public user registration means anyone can register to your server and may expose your BTCPay Server instance to potential security risks from unknown users.": "",
+ "Caution: Enabling this option, may simplify the onboarding and spending for third-parties but carries liabilities and security risks associated to storing private keys of third parties on a server.": "",
+ "Caution: Enabling this option, may simplify the onboarding for third-parties but carries liabilities and security risks associated with sharing the lightning node with other users.": "",
+ "CC": "",
+ "Celebrate payment with confetti": "",
+ "Change": "",
+ "Change connection": "",
+ "Change domain": "",
+ "Change Role": "",
+ "Change Storage provider": "",
+ "Change your {0} provider.": "",
+ "Change your password": "",
+ "Changes to the SSH settings are now permanently disabled in the BTCPay Server user interface": "",
+ "Changing the role of user {0} failed: {1}": "",
+ "Charge": "",
+ "Charge user": "",
+ "Cheat Mode: Send funds to this wallet": "",
+ "Check if NFC is supported and enabled on this device": "",
+ "Check releases on GitHub and notify when new BTCPay Server version is available": "",
+ "Checking BTCPay Server Vault is running...": "",
+ "Checking if this device can sign the transaction...": "",
+ "Checkout": "",
+ "Checkout Additional Query String": "",
+ "Checkout Appearance": "",
+ "Checkout Description": "",
+ "Checkout Experience": "",
+ "Choose a different offering for monetization": "",
+ "Choose a language...": "",
+ "Choose Point of Sale Style": "",
+ "Choose what event sends the email.": "",
+ "choose your file storage service provider": "",
+ "Choose your import method": "",
+ "Choose your signing method": "",
+ "Choose your wallet option": "",
+ "Choosing to accept an unconfirmed invoice can lead to double-spending and is strongly discouraged.": "",
+ "Claim Funds": "",
+ "Claim limit": "",
+ "Claimed": "",
+ "Claims": "",
+ "Clean": "",
+ "Clear": "",
+ "Clear All": "",
+ "Clear all filters": "",
+ "Clear all transactions from history": "",
+ "Clear filter": "",
+ "Clear label filter": "",
+ "click here": "",
+ "Clone": "",
+ "Close": "",
+ "Code": "",
+ "Coin selection": "",
+ "Collect signatures": "",
+ "Colors to rotate between with animation when a payment is made. One color per line.": "",
+ "Combine": "",
+ "Combine filters:": "",
+ "Combine PSBT": "",
+ "Comma-separated list of currencies (eg. USD,EUR,JPY)": "",
+ "Compatible wallets": "",
+ "Completed": "",
+ "CONFIDENTIAL: This QR Code is confidential, close this window as soon as you don't need it anymore.": "",
+ "Config file was not in the correct format": "",
+ "Configure": "",
+ "Configure app": "",
+ "Configure email": "",
+ "Configure now": "",
+ "Configure offering": "",
+ "Configure store email settings": "",
+ "Configure your Pay Button, and the generated code will be displayed at the bottom of the page to copy into your project.": "",
+ "Configured": "",
+ "Configuring Boltcard...": "",
+ "Confirm": "",
+ "Confirm addresses": "",
+ "Confirm broadcasting this transaction": "",
+ "Confirm in the next …": "",
+ "Confirm Lightning Payout": "",
+ "Confirm new password": "",
+ "Confirm passphrase": "",
+ "Confirm password": "",
+ "Confirmations": "",
+ "confirmed": "",
+ "Connect an existing wallet": "",
+ "Connect BTCPay Server to your Shopify checkout experience to accept Bitcoin.": "",
+ "Connect hardware wallet": "",
+ "Connect to a Lightning node": "",
+ "Connect your hardware wallet": "",
+ "Connection configuration for your custom Lightning node:": "",
+ "Connection string": "",
+ "Connection String": "",
+ "Connection to the Lightning node successful.": "",
+ "Consider the invoice paid even if the paid amount is … % less than expected": "",
+ "Consider the invoice settled when the payment transaction …": "",
+ "Constant": "",
+ "Constraints do not match any installed camera.": "",
+ "Contact URL": "",
+ "Contact Us": "",
+ "Container Name": "",
+ "Continue": "",
+ "Contribute": "",
+ "contribution": "",
+ "Contribution Amount": "",
+ "Contribution Perks Template": "",
+ "contributions": "",
+ "Contributions": "",
+ "Contributions allowed even after goal is reached": "",
+ "Contributors": "",
+ "Copy Code": "",
+ "Copy Link": "",
+ "Copy Tor URL": "",
+ "Core Lightning {0}": "",
+ "Could not access your camera. Is it already in use?": "",
+ "Could not generate invoice: {0}": "",
+ "Could not load log files": "",
+ "Could not save CSS file: {0}": "",
+ "Could not save image: {0}": "",
+ "Could not save logo: {0}": "",
+ "Could not save sound: {0}": "",
+ "Count all invoices created on the store as part of the goal": "",
+ "Create": "",
+ "Create {0} Hot Wallet": "",
+ "Create {0} Watch-Only Wallet": "",
+ "Create a new {0}": "",
+ "Create a new app": "",
+ "Create a new offering": "",
+ "Create a new store": "",
+ "Create a new subscriber": "",
+ "Create a new wallet": "",
+ "create a separate store": "",
+ "Create a store": "",
+ "Create a store to begin accepting payments.": "",
+ "Create account": "",
+ "Create Account": "",
+ "Create Email Rule": "",
+ "Create Form": "",
+ "Create Invoice": "",
+ "Create invoice to pay custom amount": "",
+ "Create Invoices": "",
+ "Create New Token": "",
+ "Create Payment Request": "",
+ "Create pending transaction": "",
+ "Create Pull Payment": "",
+ "Create refund": "",
+ "Create Request": "",
+ "Create role": "",
+ "Create Store": "",
+ "Create template from selected store": "",
+ "Create temporary file link": "",
+ "Create Token": "",
+ "Create Webhook": "",
+ "Create your account": "",
+ "Create your first store": "",
+ "Create your store": "",
+ "Created": "",
+ "Created after date:": "",
+ "Created at": "",
+ "Created before date:": "",
+ "Credentials": "",
+ "Credit": "",
+ "Credit user": "",
+ "Credits": "",
+ "Crowdfund": "",
+ "Crowdfund Behavior": "",
+ "Crypto": "",
+ "Crypto Code": "",
+ "Crypto services exposed by your server": "",
+ "CSV": "",
+ "Currency": "",
+ "Currency is invalid": "",
+ "Currency Pair Testing": "",
+ "Currency pairs to test against your rule": "",
+ "Current effective fee rate": "",
+ "Current password": "",
+ "Current Rates source is": "",
+ "Currently active!": "",
+ "Custom": "",
+ "Custom amount": "",
+ "Custom Amount": "",
+ "Custom checkout text": "",
+ "Custom CSS": "",
+ "Custom data to expand the invoice. This data is a JSON object, e.g. <code>{ \"orderId\": 615, \"product\": \"Pizza\" }</code>": "",
+ "Custom HTML title to display on Checkout page": "",
+ "Custom Payments": "",
+ "Custom Range": "",
+ "Custom sound file for successful payment": "",
+ "Custom text displayed on the checkout page below the payment details. Plain text only, newlines are supported.": "",
+ "Custom Theme Extension Type": "",
+ "Custom Theme File": "",
+ "Customer Email": "",
+ "Customer Information": "",
+ "Customization": "",
+ "Customize Pay Button Text": "",
+ "Dark": "",
+ "Dashboard": "",
+ "Date": "",
+ "days": "",
+ "Decode PSBT": "",
+ "Default": "",
+ "Default currency": "",
+ "Default Currency Pairs": "",
+ "Default dictionary changed to {0}": "",
+ "Default Include NonWitness Utxo in PSBTs": "",
+ "Default language on checkout": "",
+ "Default Payment Method": "",
+ "Default payment method on checkout": "",
+ "Default role for users on a new store": "",
+ "Default store template": "",
+ "Default Tax Rate": "",
+ "Default Value": "",
+ "Delay": "",
+ "Delete": "",
+ "DELETE": "",
+ "Delete Account": "",
+ "Delete admin": "",
+ "Delete API key": "",
+ "Delete app": "",
+ "Delete dictionary": "",
+ "Delete label": "",
+ "Delete LND seed": "",
+ "Delete LND seed from server": "",
+ "Delete offering": "",
+ "Delete role": "",
+ "Delete store": "",
+ "Delete store {0}": "",
+ "Delete this app": "",
+ "Delete this store": "",
+ "Delete unused Docker images present on your system.": "",
+ "Delete user": "",
+ "Delete Webhook": "",
+ "Delivered at": "",
+ "Demonetize": "",
+ "Dependencies": "",
+ "Dependencies not met.": "",
+ "Derivation scheme": "",
+ "Description": "",
+ "Description template of the lightning invoice": "",
+ "Destination": "",
+ "Destination Address": "",
+ "Details": "",
+ "Detects the language of the customer's browser.": "",
+ "Determine the generated invoice amount": "",
+ "Determine the generated invoice currency": "",
+ "Device found: {0}": "",
+ "Dictionaries": "",
+ "Dictionaries enable you to translate the BTCPay Server backend into different languages.": "",
+ "Dictionary": "",
+ "Dictionary {0} deleted": "",
+ "Dictionary created": "",
+ "Dictionary updated": "",
+ "Direct integration": "",
+ "Disable": "",
+ "DISABLE": "",
+ "Disable 2FA": "",
+ "Disable admin": "",
+ "Disable all notifications": "",
+ "Disable modification of SSH settings": "",
+ "Disable payment button": "",
+ "Disable public user registration": "",
+ "Disable stores from using the server's email settings as backup": "",
+ "Disable two-factor authentication (2FA)": "",
+ "Disable zero amount invoices": "",
+ "Disabled": "",
+ "Disabled Plugins": "",
+ "Disabling 2FA does not change the keys used in the authenticator apps. If you wish to change the key used in an authenticator app you should reset your authenticator keys.": "",
+ "Disabling will delete your rate script.": "",
+ "Discount": "",
+ "Discounts": "",
+ "Discourage search engines from indexing this site": "",
+ "Discussion": "",
+ "Display app on website root": "",
+ "Display contribution ranking": "",
+ "Display contribution value": "",
+ "Display item selection for keypad": "",
+ "Display Lightning payment amounts in Satoshis": "",
+ "Display Options": "",
+ "Display the category list": "",
+ "Display the key or QR code to configure an authenticator app with your current setup.": "",
+ "Display the search bar": "",
+ "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?": "",
+ "Docs": "",
+ "Documentation": "",
+ "Does not extend a BTCPay Server theme, fully custom": "",
+ "Domain": "",
+ "Domain name": "",
+ "Domain name changing... the server will restart, please use \"{0}\" (this page won't reload automatically)": "",
+ "Domain to app mapping": "",
+ "Don't create UTXO change": "",
+ "Donate": "",
+ "Done": "",
+ "Downgrade": "",
+ "Download": "",
+ "Download a two-factor authenticator app like …": "",
+ "Download language pack": "",
+ "Download Language Pack": "",
+ "Download PSBT file": "",
+ "Dynamic": "",
+ "Dynamic DNS allows you to have a stable DNS name pointing to your server, even if your IP address changes regularly. This is recommended if you are hosting BTCPay Server at home and wish to have a clearnet domain to access your server.": "",
+ "Dynamic DNS Service": "",
+ "Dynamic DNS service successfully removed": "",
+ "Dynamic DNS Settings": "",
+ "Each address generated will be imported into the node wallet and you can view your balance through the node.": "",
+ "Each payment method shows the total excess amount.": "",
+ "Easily filter the different items using categories, used only in the product list with cart.": "",
+ "Easily log into BTCPay Server on another device using a simple login code from an already authenticated device.": "",
+ "Edit": "",
+ "Edit Field": "",
+ "Edit Form": "",
+ "Edit Item": "",
+ "Edit Label": "",
+ "Edit payment request": "",
+ "Edit Payment Request": "",
+ "Edit plan": "",
+ "Edit plan ({0})": "",
+ "Edit pull payment": "",
+ "Edit Pull Payment": "",
+ "Editor": "",
+ "Either your {0} wallet is not configured, or it is not a hot wallet. This processor cannot function until a hot wallet is configured in your store.": "",
+ "Email": "",
+ "Email address": "",
+ "Email address is confirmed": "",
+ "Email Configuration": "",
+ "Email confirmation required": "",
+ "Email Confirmed": "",
+ "Email confirmed?": "",
+ "Email Notifications": "",
+ "Email password reset functionality is not configured for this server. Please contact the server administrator to assist with account recovery.": "",
+ "Email Reminder Days Before Due": "",
+ "Email rule successfully created": "",
+ "Email rule successfully deleted": "",
+ "Email rule successfully updated": "",
+ "Email rules": "",
+ "Email Rules": "",
+ "Email rules allow BTCPay Server to send customized emails from your server based on events.": "",
+ "Email rules allow BTCPay Server to send customized emails from your store based on events.": "",
+ "Email sent to {0}. Please verify you received it.": "",
+ "Email server password reset": "",
+ "Email settings saved": "",
+ "Emails": "",
+ "Embed a payment button linking to POS item": "",
+ "Embed Point of Sale via iframe": "",
+ "Empty": "",
+ "Enable": "",
+ "Enable 2FA": "",
+ "Enable advanced rate rule scripting": "",
+ "Enable Authenticator App": "",
+ "Enable background animations on new payments": "",
+ "Enable Disqus Comments": "",
+ "Enable experimental features": "",
+ "Enable fallback rates": "",
+ "Enable LNURL": "",
+ "Enable notifications": "",
+ "Enable PayJoin": "",
+ "Enable Payjoin/P2EP": "",
+ "Enable payment methods only when amount is …": "",
+ "Enable public receipt page for settled invoices": "",
+ "Enable public user registration": "",
+ "Enable sounds on checkout page": "",
+ "Enable sounds on new payments": "",
+ "Enable tips": "",
+ "Enable trial": "",
+ "Enabled": "",
+ "Enabling will modify your current rate sources. This is a feature for advanced users.": "",
+ "End date": "",
+ "End Date": "",
+ "Ends {0}": "",
+ "Ends in": "",
+ "Enhance the checkout process for in-store purchases.<br />This assumes the payment page will be displayed on the merchant's device.": "",
+ "Enhance the checkout process for online purchases.<br />This assumes the payment page will be displayed on the customer's device.": "",
+ "Enter destination to claim funds": "",
+ "Enter extended public key": "",
+ "Enter the code in the confirmation box below.": "",
+ "Enter the passphrase.": "",
+ "Enter the pin.": "",
+ "Enter the wallet seed": "",
+ "Enter wallet seed": "",
+ "Enter your extended public key": "",
+ "Error": "",
+ "Error updating profile": "",
+ "Error updating user": "",
+ "Error while broadcasting: {0}": "",
+ "Example": "",
+ "Expiration Date": "",
+ "Expire": "",
+ "Expire invoice in …": "",
+ "Expired": "",
+ "expired with partial payments": "",
+ "Expires in": "",
+ "Export": "",
+ "Export the PSBT for your wallet. Sign it with your wallet and import the signed PSBT version here for finalization and broadcasting.": "",
+ "Extended public key": "",
+ "Extends the BTCPay Server Dark theme": "",
+ "Extends the BTCPay Server Light theme": "",
+ "External payout approval": "",
+ "Failed to archive the app.": "",
+ "Failed to download language pack: {0}": "",
+ "Failed to unarchive the app.": "",
+ "Fallback": "",
+ "Fallback rate source": "",
+ "Fallback rates will be used in case the primary rates are not available.": "",
+ "Feature disabled": "",
+ "Featured Image URL": "",
+ "Features": "",
+ "Fee block target": "",
+ "Fee bump method": "",
+ "Fee rate": "",
+ "Fee rate (sat/vB)": "",
+ "Fee will be shown for BTC and LTC onchain payments only.": "",
+ "Fetching device...": "",
+ "Fetching public keys...": "",
+ "Fetching wallet's fingerprint.": "",
+ "FIDO2 Authentication": "",
+ "Field to mirror": "",
+ "File Id": "",
+ "File Logging Option not specified. You need to set debuglog and optionally debugloglevel in the configuration or through runtime arguments": "",
+ "File name": "",
+ "File removed": "",
+ "file storage": "",
+ "File Storage": "",
+ "file storage service": "",
+ "File with wallet password and seed info not present": "",
+ "Files": "",
+ "Files added successfully": "",
+ "Files could not be added due to invalid names": "",
+ "Files uploaded, restart server to load plugins": "",
+ "Fill fake": "",
+ "Fill out currency pair to test for (like {0})": "",
+ "Filter": "",
+ "Filter by address, label or date": "",
+ "Filter by label": "",
+ "Filter by transaction id, amount, label, comment": "",
+ "Filter invoices by Custom Range": "",
+ "Filter payment requests by Custom Range": "",
+ "Firstname Lastname <email@example.com>": "",
+ "Fit button inline": "",
+ "Fixed amount": "",
+ "follow these instructions": "",
+ "For a specific item of your template": "",
+ "For anything with a custom amount": "",
+ "for lifetime": "",
+ "For many email providers (like Gmail) your login is your email address.": "",
+ "For the macaroon options you need to provide a macaroon with the <code>invoices:write</code> permission (e.g. <code>invoice.macaroon</code>. If you want to display the node connection details, it also needs the <code>info:read</code> permission.": "",
+ "For wallet compatibility: Bech32 encoded (classic) vs. cleartext URL (upcoming)": "",
+ "Forgot password?": "",
+ "Form config was invalid: {0}": "",
+ "Form configuration (JSON)": "",
+ "Form created successfully.": "",
+ "Form JSON": "",
+ "Form removed": "",
+ "Form updated successfully.": "",
+ "Forms": "",
+ "Free": "",
+ "Full node connection": "",
+ "Gap limit": "",
+ "General": "",
+ "General Settings": "",
+ "General text search:": "",
+ "Generate": "",
+ "Generate {0} Wallet": "",
+ "Generate a brand-new wallet to use": "",
+ "Generate a new api key to use BTCPay through its API.": "",
+ "Generate a QR code of the extended public key in your wallet (see instructions for supported wallets below).\n Allow the browser access to your camera and hold the code to the camera when the scan prompt appears.": "",
+ "Generate another address": "",
+ "Generate API Key": "",
+ "Generate Key": "",
+ "Generated Code": "",
+ "Get Link": "",
+ "Give other registered BTCPay Server users access to your store. See the {0} for granted permissions.": "",
+ "Go back to Javascript enabled invoice": "",
+ "Go to email rules": "",
+ "Go to top": "",
+ "Google Cloud Storage": "",
+ "Grace": "",
+ "Grace Period": "",
+ "Grace Period (days)": "",
+ "Greater than": "",
+ "Greenfield API": "",
+ "Greenfield API Keys": "",
+ "GRPC SSL Cipher suite (GRPC_SSL_CIPHER_SUITES)": "",
+ "Hardcap Goal": "",
+ "Hardware wallet": "",
+ "Has at least 1 confirmation": "",
+ "Has at least 2 confirmations": "",
+ "Has at least 6 confirmations": "",
+ "has payments that failed to confirm on time": "",
+ "Has Store": "",
+ "Helper Text": "",
+ "here": "",
+ "Hide coin selection": "",
+ "Hide Sensitive Info": "",
+ "Hide unconfirmed coins": "",
+ "Hostname": "",
+ "Hot wallet": "",
+ "How much to refund?": "",
+ "However, <code>kraken</code> does not support the <code>BTC_CAD</code> pair. For this reason you can add a rule\n mapping all <code>X_CAD</code> to <code>ndax</code>, a Canadian exchange.": "",
+ "However, explicitely setting specific pairs like this can be a bit difficult. Instead, you can define a rule\n <code>X_X</code>\n which will match any currency pair. The following example will use <code>kraken</code> for getting the rate of any currency pair.": "",
+ "HTML Headers": "",
+ "HTML Lang": "",
+ "HTML Meta Tags": "",
+ "HTTP-based Tor hidden services": "",
+ "I have written down my recovery phrase and stored it in a secure location": "",
+ "I wrote down my recovery codes": "",
+ "Id": "",
+ "ID": "",
+ "If a translation isn’t available in the new dictionary, it will be searched in the fallback.": "",
+ "If authorized, the generated API key will be provided to": "",
+ "If BTCPay Server shows you an invalid balance, {0}.<br />If some transactions appear in BTCPay Server, but are missing in another wallet, {1}.": "",
+ "If checked, each private key associated with an address generated will be stored as metadata and would be accessible to anyone with admin access to your server. Enable at your own risk!": "",
+ "If you change your configured storage provider, your current files will become inaccessible.": "",
+ "If you do not understand the above, go with the defaults and start scanning.": "",
+ "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.": "",
+ "If you lose your device and don't have the recovery codes you will lose access to your account.": "",
+ "If your LND REST server is using HTTP or HTTPS with an untrusted certificate, you can set\n <code>allowinsecure=true</code> as a fallback.": "",
+ "If your security device has a button, tap on it.": "",
+ "Image": "",
+ "Image Size": "",
+ "Image uploaded successfully": "",
+ "Image Url": "",
+ "Import {0} Wallet": "",
+ "Import an existing hardware or software wallet": "",
+ "Import failed, make sure you import a compatible wallet format": "",
+ "Import keys to RPC": "",
+ "Import wallet file": "",
+ "Import your public keys using our Vault application": "",
+ "Import your wallet file": "",
+ "Important notice": "",
+ "Important notice about plugins": "",
+ "Impossible to fetch rate: {0}": "",
+ "In order to securely connect to your hardware wallet you must first download, install, and run the BTCPay Server Vault.": "",
+ "In order to upload, a {0} must be configured.": "",
+ "In Shopify, please do the following …": "",
+ "In use": "",
+ "In-store": "",
+ "Inactive": "",
+ "Include archived": "",
+ "Include Archived": "",
+ "Incorrect pin code.": "",
+ "Increase the security of your instance by disabling the ability to change the SSH settings in this BTCPay Server instance's user interface.": "",
+ "Index": "",
+ "Input the key string manually": "",
+ "Inputs": "",
+ "Insert your security device and proceed.": "",
+ "Install": "",
+ "Installed Plugins": "",
+ "Instructions": "",
+ "Interval": "",
+ "Invalid": "",
+ "Invalid App Type": "",
+ "Invalid Base URL": "",
+ "Invalid currency": "",
+ "Invalid currency pair '{0}' (it should be formatted like {1})": "",
+ "Invalid currency pairs (should be for example: {0})": "",
+ "Invalid destination or payment method": "",
+ "Invalid email": "",
+ "Invalid email address or placeholder detected": "",
+ "Invalid file name": "",
+ "Invalid login attempt.": "",
+ "Invalid network": "",
+ "Invalid passphrase confirmation": "",
+ "Invalid password confirmation.": "",
+ "Invalid payout method": "",
+ "Invalid PSBT": "",
+ "Invalid role": "",
+ "Invalid store": "",
+ "Invalid wallet format: {0}": "",
+ "Invalidates the current authenticator configuration. Useful if you believe your authenticator settings were compromised.": "",
+ "Inventory": "",
+ "Inventory for {0} exhausted: {1} available": "",
+ "Invitation accepted. Please set your password.": "",
+ "Invitation URL": "",
+ "Invoice": "",
+ "Invoice {0}": "",
+ "Invoice {0} just created!": "",
+ "Invoice {0}..": "",
+ "Invoice currency": "",
+ "Invoice expires if the full amount has not been paid after …": "",
+ "Invoice Id": "",
+ "Invoice is not overpaid": "",
+ "Invoice metadata": "",
+ "Invoice Notifications": "",
+ "Invoices": "",
+ "Invoices are documents issued by the seller to a buyer to collect payment.": "",
+ "Is Admin": "",
+ "Is administrator?": "",
+ "Is hot wallet": "",
+ "Is MultiSig on Server": "",
+ "Is Set Up": "",
+ "is settled": "",
+ "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 />It means that the\n rule <code>USD_DOGE = 1 / DOGE_USD</code> implicitly exists.": "",
+ "Item Description": "",
+ "Items total": "",
+ "JSON": "",
+ "JSON Credentials": "",
+ "Keep empty for server-initiated pairing.": "",
+ "Key": "",
+ "Key path": "",
+ "Keypad": "",
+ "Label": "",
+ "Label Name": "",
+ "Label name cannot be empty.": "",
+ "Label:": "",
+ "Labels": "",
+ "Language pack '{0}' downloaded successfully": "",
+ "Language pack '{0}' updated successfully": "",
+ "Last 24 hours": "",
+ "Last 3 days": "",
+ "Last 7 days": "",
+ "Last delivery {0}": "",
+ "Last error:": "",
+ "Last updated": "",
+ "Last Updated": "",
+ "Learn More": "",
+ "Leave blank to generate ID from title.": "",
+ "Leave blank to not use this feature.": "",
+ "Legacy": "",
+ "Legacy (Not recommended)": "",
+ "Legacy API Keys": "",
+ "Less than": "",
+ "Let's get started": "",
+ "Light": "",
+ "Lightning Address": "",
+ "Lightning address {0} removed successfully.": "",
+ "Lightning address added successfully.": "",
+ "Lightning Balance": "",
+ "Lightning charge is a simple API for invoicing on lightning network, you can use it with several plugins:": "",
+ "Lightning Charge Service": "",
+ "Lightning Enabled": "",
+ "Lightning network settings": "",
+ "Lightning node (LNURL Auth)": "",
+ "Lightning Payout Processor": "",
+ "Lightning Payout Result": "",
+ "Lightning Services": "",
+ "Lightning Supported": "",
+ "Limit": "",
+ "Link": "",
+ "Link this Pay Button to an app instead. Some features are disabled due to the different endpoint capabilities. You can set which perk/item this button should be targeting.": "",
+ "Link URL": "",
+ "LND {0}": "",
+ "LND Seed Backup": "",
+ "LNURL": "",
+ "LNURL Auth was removed successfully.": "",
+ "LNURL Authentication": "",
+ "LNURL Classic Mode": "",
+ "LNURL is required for lightning addresses but has not yet been enabled.": "",
+ "LNURL or LN is disabled": "",
+ "LNURL-Withdraw": "",
+ "Loading...": "",
+ "Local File System": "",
+ "Local Filesystem Storage": "",
+ "Log in": "",
+ "Login": "",
+ "Login code was invalid": "",
+ "Login Codes": "",
+ "Logo": "",
+ "Logout": "",
+ "Logs": "",
+ "Mails": "",
+ "Maintenance": "",
+ "Maintenance feature requires access to SSH properly configured in BTCPay Server configuration.": "",
+ "Make Crowdfund Public": "",
+ "Make sure this BTCPay Server instance belongs to you.": "",
+ "Manage": "",
+ "Manage Account": "",
+ "Manage billing": "",
+ "Manage labels": "",
+ "Manage Labels": "",
+ "Manage Plugins": "",
+ "Manually enter your 12 or 24 word recovery seed.": "",
+ "Map specific domains to specific apps": "",
+ "Mapped Value": "",
+ "Mark as a test account": "",
+ "Mark as already paid": "",
+ "Mark as awaiting payment": "",
+ "Mark as invalid": "",
+ "Mark as seen": "",
+ "Mark as settled": "",
+ "Marked for deletion": "",
+ "Marked for enabling": "",
+ "Master fingerprint": "",
+ "Max": "",
+ "Max sats": "",
+ "Maximum amount of sats to allow to be sent to this ln address": "",
+ "Maximum amount:": "",
+ "Memo": "",
+ "Message": "",
+ "Metadata": "",
+ "Metadata must be a valid JSON object": "",
+ "Metadata was not valid JSON": "",
+ "Method": "",
+ "Migrate existing non-admin users": "",
+ "Migrate existing users": "",
+ "Min": "",
+ "Min sats": "",
+ "Mine": "",
+ "Mine to test processing and settlement": "",
+ "Minimum acceptable expiration time for BOLT11 for refunds": "",
+ "Minimum amount of sats to allow to be sent to this ln address": "",
+ "Minimum amount:": "",
+ "minutes": "",
+ "Mirror of": "",
+ "Modify": "",
+ "Monetization": "",
+ "Monetization allows you to get paid for sharing your BTCPay Server instance with other users.": "",
+ "Monetization deactivated, users who register to your server from now will not be subscriber of any offering.": "",
+ "Monetization order updated to offering {0} with default plan {1}.": "",
+ "Monthly revenue": "",
+ "More details": "",
+ "More details...": "",
+ "More information": "",
+ "More information...": "",
+ "Name": "",
+ "Navigate back to home": "",
+ "NBXplorer headers height: {0}": "",
+ "NBXplorer is synchronizing... (Height: {0})": "",
+ "NBXplorer is unable to track this derivation scheme. You may need to update it.": "",
+ "Negative amount is not allowed": "",
+ "Negative tip or discount is not allowed": "",
+ "Network Fee": "",
+ "Never add network fee": "",
+ "New {0} plugin version {1} released!": "",
+ "New Card": "",
+ "New effective fee rate": "",
+ "New offering": "",
+ "New offering created. You can now <a href='{0}' class='alert-link'>configure it.</a>": "",
+ "New password": "",
+ "New plan created": "",
+ "New role": "",
+ "New user {0} requires approval.": "",
+ "New user requires approval": "",
+ "New version": "",
+ "New Version": "",
+ "New version {0} released!": "",
+ "Next": "",
+ "NFC detected.": "",
+ "No": "",
+ "No access tokens yet.": "",
+ "No claim made yet.": "",
+ "No contributions allowed after the goal has been reached": "",
+ "No contributions have been made yet.": "",
+ "No deliveries for this webhook yet": "",
+ "No device connected.": "",
+ "No documentation": "",
+ "No email address has been configured for the Server. Configure an email address\n to\n begin sending emails.": "",
+ "No end date has been set": "",
+ "No expiry date has been set for this payment request": "",
+ "No invoice has been selected": "",
+ "No matching payment method": "",
+ "No payment handler found for this payment method": "",
+ "No payment request related email rules have been configured for this store.": "",
+ "No payments have been made yet.": "",
+ "No payout selected": "",
+ "No permissions": "",
+ "No plugins found": "",
+ "No policies": "",
+ "No public address available.": "",
+ "No public address has been configured.": "",
+ "No reserved addresses found.": "",
+ "No sales have been made yet.": "",
+ "No scope": "",
+ "No start date has been set": "",
+ "No stores": "",
+ "No transaction selected": "",
+ "No unpaid pending invoice to cancel": "",
+ "No users": "",
+ "Node headers height: {0}": "",
+ "Node Info": "",
+ "Non-admins can access the User Creation API Endpoint": "",
+ "Non-admins can create Cold Wallets for their Store": "",
+ "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": "",
+ "Non-supported state of invoice": "",
+ "None of the selected transaction can be fee bumped": "",
+ "Normal": "",
+ "Not all payout methods are supported": "",
+ "Not allowed to cancel this invoice": "",
+ "Not available in Keypad POS": "",
+ "Not configured": "",
+ "Not recommended": "",
+ "Not recommended for customer self-checkout.": "",
+ "Note that <code>bitcoin-host</code> and <code>bitcoin-auth</code> are optional, only useful if you want to use <code>GetDepositAddress</code>\n on Eclair:": "",
+ "Note: {0} are still immature and require additional confirmations.": "",
+ "Notification Email": "",
+ "Notification Settings": "",
+ "Notification URL": "",
+ "Notification URL Callbacks": "",
+ "Notifications": "",
+ "Notifications & Alerts": "",
+ "Offering ({0})": "",
+ "Offering configuration updated": "",
+ "Offline signing, without connecting your wallet to the internet": "",
+ "On-Chain Payments": "",
+ "On-Chain Payout Processor": "",
+ "Online": "",
+ "Only enable the payment method after user explicitly chooses it": "",
+ "Only meta tags are allowed in HTML headers. Your HTML code has been cleaned up accordingly.": "",
+ "Only process payouts when this payout sum is reached.": "",
+ "Only send email when the specified JSON Path exists": "",
+ "Only upload plugins from trusted sources.": "",
+ "Open in wallet": "",
+ "Optimistic activation": "",
+ "optional": "",
+ "Optional passphrase (BIP39)": "",
+ "Optional seed passphrase": "",
+ "Optional: Specify the percentage by which to reduce the refund, e.g. as processing charge or to compensate for the mining fee.": "",
+ "Options": "",
+ "or": "",
+ "or more": "",
+ "Order Id": "",
+ "Order ID": "",
+ "Original transaction": "",
+ "Original Value": "",
+ "Other actions": "",
+ "Other external services": "",
+ "Other Tor hidden services": "",
+ "Otherwise you are exposing yourself to malicious site owners, or to malicious plugins installed in your browser.": "",
+ "Otherwise, the server's SMTP settings will be used to send emails.": "",
+ "Outputs": "",
+ "Overpaid": "",
+ "Overpaid amount": "",
+ "Overpaid amount cannot be calculated": "",
+ "Override the block explorers used": "",
+ "Page Size": "",
+ "Page Size:": "",
+ "Paid": "",
+ "Paid invoices in the last {0} days": "",
+ "Pair to": "",
+ "Pair To Store": "",
+ "Pairing Permission": "",
+ "Parsing error: {0}": "",
+ "Partially Signed Bitcoin Transaction": "",
+ "Passphrase (Leave empty if there isn't any passphrase)": "",
+ "Passphrase confirmation": "",
+ "Password": "",
+ "Password (leave blank to generate invite-link)": "",
+ "Password entered...": "",
+ "Password Reset": "",
+ "Password successfully set": "",
+ "Password successfully set.": "",
+ "Paste BIP21": "",
+ "Pay": "",
+ "Pay Button": "",
+ "Pay Button Image Url": "",
+ "Pay Button request failed": "",
+ "Pay Button Text": "",
+ "Pay Invoice": "",
+ "Paying via this payment method is not supported": "",
+ "PayJoin BIP21": "",
+ "PayJoin enhances the privacy for you and your customers. Enabling it gives your customers the option to use PayJoin during checkout.": "",
+ "Payjoin transaction": "",
+ "Payload URL": "",
+ "Payment": "",
+ "Payment cancelled": "",
+ "Payment Details": "",
+ "Payment History": "",
+ "Payment invalid if transactions fails to confirm … after invoice expiration": "",
+ "Payment Link": "",
+ "Payment method": "",
+ "Payment Method": "",
+ "Payment Notifications": "",
+ "Payment Proof": "",
+ "Payment received, waiting for confirmation...": "",
+ "Payment request \"{0}\" created successfully": "",
+ "Payment request \"{0}\" updated successfully": "",
+ "Payment Request cannot be paid as it has been archived": "",
+ "Payment Request has already been settled.": "",
+ "Payment Request has expired": "",
+ "Payment Request Labels": "",
+ "payment requests": "",
+ "Payment Requests": "",
+ "Payment requests are persistent shareable pages that enable the receiver to pay at their convenience. Funds are paid to a payment request at the current exchange rate.": "",
+ "Payments": "",
+ "Payout Methods": "",
+ "Payout Processor removed": "",
+ "Payout Processors": "",
+ "Payout Processors allow BTCPay Server to handle payouts in an automated way.": "",
+ "Payout Processors help automate payouts so that you do not need to manually handle them.": "",
+ "Payouts": "",
+ "Payouts allow you to process pull payments, in the form of refunds, salary payouts, or withdrawals.": "",
+ "Payouts approved": "",
+ "Payouts archived": "",
+ "Payouts for pull payment {0}": "",
+ "Payouts marked as paid": "",
+ "Payouts Pending": "",
+ "Pending": "",
+ "Pending Action": "",
+ "Pending actions": "",
+ "Pending Actions": "",
+ "Pending Approval": "",
+ "Pending Email Verification": "",
+ "Pending Invitation": "",
+ "per month": "",
+ "per quarter": "",
+ "per year": "",
+ "percent": "",
+ "Percentage must be a numeric value between 0 and 100": "",
+ "Permanent Url": "",
+ "Permissions": "",
+ "Phase": "",
+ "Pin code verified.": "",
+ "Placeholder": "",
+ "Placeholders": "",
+ "Plan": "",
+ "Plan deleted": "",
+ "Plan edited": "",
+ "Plan ID": "",
+ "Plan Name": "",
+ "Plans": "",
+ "Please check that your wallet is generating the same addresses as below.": "",
+ "Please check your addresses and confirm.": "",
+ "Please check your email to reset your password.": "",
+ "Please configure it first.": "",
+ "Please consult the server log for more details.": "",
+ "Please contact support.": "",
+ "Please enable JavaScript for this option to be available": "",
+ "Please enter a positive amount": "",
+ "Please fix errors shown in order for code generation to successfully execute.": "",
+ "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.": "",
+ "Please note that creating a wallet is not supported by your instance.": "",
+ "Please note that not all text is translatable, and future updates may modify existing translations or introduce new translatable phrases.": "",
+ "Please note that this instance does not support creating a new cold wallet for non-administrators. However, you can import one from other wallet software.": "",
+ "Please provide a connection string": "",
+ "Please provide a destination": "",
+ "Please provide an amount greater than 0": "",
+ "Please provide your existing seed": "",
+ "Please provide your extended public key": "",
+ "Please remove the NFC from the card reader": "",
+ "Please review and confirm the transaction on your device...": "",
+ "Please select a language": "",
+ "Please select an option before proceeding": "",
+ "Please set NBXPlorer's PostgreSQL connection string to make this feature available.": "",
+ "Please verify that the address displayed on your device is <b>{0}</b>...": "",
+ "Please wait for your node to be synched": "",
+ "Please, confirm on the device first...": "",
+ "Please, enter the passphrase on the device.": "",
+ "Plugin action cancelled.": "",
+ "Plugin scheduled to be enabled.": "",
+ "Plugin scheduled to be installed.": "",
+ "Plugin scheduled to be uninstalled.": "",
+ "Plugin server": "",
+ "Plugin update": "",
+ "Plugin Updates": "",
+ "Plugins": "",
+ "Plugins are developed by third parties. They need to be updated and maintained regularly in addition to BTCPay Server. Use plugins at your own risk.": "",
+ "Point of Sale": "",
+ "Point of Sale Style": "",
+ "Policies": "",
+ "Policies updated successfully": "",
+ "Port": "",
+ "Powered by": "",
+ "Preferred Price Source": "",
+ "Prev": "",
+ "Preview": "",
+ "Price": "",
+ "Price must be greater than 0": "",
+ "Primary rate source": "",
+ "Print": "",
+ "Print display": "",
+ "Private key or seed": "",
+ "Pro tip: There are supported but unconfigured Payout Processors for this payout payment method.": "",
+ "Proceed": "",
+ "Proceed to free trial": "",
+ "Proceed to Secure Payment": "",
+ "Process approved payouts instantly": "",
+ "Processing": "",
+ "Processor updated.": "",
+ "Product list": "",
+ "Product list with cart": "",
+ "Profile Picture": "",
+ "Provide the 12 or 24 word recovery seed": "",
+ "Provide updated PSBT": "",
+ "Provider": "",
+ "Prune old transactions from history": "",
+ "PSBT content": "",
+ "PSBT Successfully combined!": "",
+ "PSBT to combine with…": "",
+ "PSBT too large to be signed by Vault. (Max: {0} bytes)": "",
+ "PSBT updated!": "",
+ "Public Key": "",
+ "Public keys successfully fetched.": "",
+ "Public Node Info": "",
+ "Pull payment archived": "",
+ "Pull payment request created": "",
+ "Pull payment updated successfully": "",
+ "Pull Payments": "",
+ "Pull Payments allow receivers to claim specified funds from your wallet at their convenience. Once submitted and approved, the funds will be released.": "",
+ "Put these codes in a safe place": "",
+ "QR Code connection": "",
+ "QR Code data": "",
+ "QR import failed: {0}": "",
+ "Qty": "",
+ "Query pairs via REST by querying {0} without the need to specify currencyPairs.": "",
+ "Quick Fill": "",
+ "Rate": "",
+ "Rate rule scripting": "",
+ "Rate Rules": "",
+ "Rate rules scripting activated": "",
+ "Rate rules scripting deactivated": "",
+ "Rate script allows you to express precisely how you want to calculate rates for currency pairs.": "",
+ "Rate settings updated": "",
+ "Rate Source": "",
+ "Rate Spread": "",
+ "Rate unavailable: {0}": "",
+ "Rates": "",
+ "Re-enabling will not require you to reconfigure your app.": "",
+ "Read more": "",
+ "Receipt": "",
+ "Receive": "",
+ "Receive {0}": "",
+ "Receive email notification updates.": "",
+ "Receive updates for this invoice.": "",
+ "Recent deliveries": "",
+ "Recent Invoices": "",
+ "Recent Transactions": "",
+ "Recommendation ({0})": "",
+ "Recommended": "",
+ "Recommended fee confirmation target blocks": "",
+ "Recovery Code": "",
+ "Recovery codes": "",
+ "Recurring": "",
+ "Recurring Goal": "",
+ "Recurring Type": "",
+ "Redeliver": "",
+ "Redirect invoice to redirect url automatically after paid": "",
+ "Redirect URL": "",
+ "Redirects": "",
+ "Refer to this guide to get started with Shopify V2": "",
+ "Reference Id": "",
+ "Refund": "",
+ "Refund {0}": "",
+ "Refunds Issued": "",
+ "Regenerate": "",
+ "Regenerate your 2FA recovery codes.": "",
+ "Register": "",
+ "Register Device": "",
+ "Register page redirect URL": "",
+ "Register wallet for payment links": "",
+ "Register your Lightning node for LNURL Auth": "",
+ "Register your security device": "",
+ "Registration": "",
+ "Remember me": "",
+ "Remember this machine": "",
+ "Remote plugins lookup failed. Try again later. Error: {0}": "",
+ "Remove": "",
+ "Remove {0} wallet": "",
+ "Remove Destination": "",
+ "Remove domain mapping": "",
+ "Remove email rule": "",
+ "Remove Lightning Address": "",
+ "Remove Lightning security": "",
+ "Remove LNURL Auth link": "",
+ "Remove Mapping": "",
+ "Remove plan": "",
+ "Remove security device": "",
+ "Remove Store Permission": "",
+ "Remove store template": "",
+ "Remove store user": "",
+ "Remove the translation from this dictionary.": "",
+ "Remove wallet": "",
+ "Removing this user would result in the store having no owner.": "",
+ "Renewable": "",
+ "REPLACE": "",
+ "Replace {0} wallet": "",
+ "Replace template from selected store": "",
+ "Replace wallet": "",
+ "Replacements": "",
+ "Reporting": "",
+ "Request contributor data on checkout": "",
+ "Request customer data on checkout": "",
+ "Request Pairing": "",
+ "Requests": "",
+ "Requests may be paid in partial. They will remain valid until time expires or when paid what is due.": "",
+ "Required Field": "",
+ "Rescan Wallet": "",
+ "Rescan wallet for missing transactions": "",
+ "rescan your wallet": "",
+ "Resend email": "",
+ "Reserved Addresses": "",
+ "Reserved At": "",
+ "Reset": "",
+ "RESET": "",
+ "Reset app": "",
+ "Reset authenticator app": "",
+ "Reset Boltcard": "",
+ "Reset goal after a specific period of time, based on your crowdfund's start date.": "",
+ "Reset goal every": "",
+ "Reset Password": "",
+ "Reset recovery codes": "",
+ "Reset your password": "",
+ "Resetting Boltcard...": "",
+ "Resources": "",
+ "REST Uri": "",
+ "Restart": "",
+ "Restart BTCPay Server and related services.": "",
+ "Restart now": "",
+ "Retired": "",
+ "Retiring": "",
+ "Retry": "",
+ "Reveal": "",
+ "Revoke": "",
+ "Revoke access token": "",
+ "Revoke the token": "",
+ "Role": "",
+ "Role could not be set as default": "",
+ "Role could not be updated": "",
+ "Role created": "",
+ "Role deleted": "",
+ "Role set default": "",
+ "Role updated": "",
+ "roles": "",
+ "Roles": "",
+ "Root fingerprint": "",
+ "RPC Error while broadcasting: {0}": "",
+ "sale": "",
+ "sales": "",
+ "Sales": "",
+ "Save": "",
+ "Save comment": "",
+ "Save Wallet Settings": "",
+ "Saving...": "",
+ "Scan destination with camera": "",
+ "Scan Login code with camera": "",
+ "Scan QR code": "",
+ "Scan the extended public key, also called \"xpub\", shown on your wallet's display.": "",
+ "Scan the QR Code or enter the following key into your two-factor authenticator app:": "",
+ "Scan the QR code to open the Point of Sale": "",
+ "Scan the QR code to open this page on a mobile": "",
+ "Scan the QR code with your Lightning wallet to link it to your user account.": "",
+ "Scan the QR code with your Lightning wallet to sign in.": "",
+ "Scan wallet QR code": "",
+ "Scan wallet QR with camera": "",
+ "Scan with camera": "",
+ "Scanning the UTXO set allow you to restore the balance of your wallet, but not all the transaction history. This operation will scan the HD Path <b>0/*</b>, <b>1/*</b> and <b>*</b> from a starting index, until no UTXO are found in a whole gap limit.": "",
+ "Schedule install": "",
+ "Schedule transaction": "",
+ "Schedule update": "",
+ "Scope": "",
+ "Scripting": "",
+ "Search": "",
+ "Search by email, external reference, name…": "",
+ "Search by email...": "",
+ "Search by Id, Title or Amount...": "",
+ "Search engines can index this site": "",
+ "Search…": "",
+ "seconds": "",
+ "Secure your recovery phrase": "",
+ "Security device (FIDO2)": "",
+ "Security device name": "",
+ "Security devices": "",
+ "See confidential seed information": "",
+ "See information": "",
+ "See QR Code information by clicking": "",
+ "Seed information was already removed": "",
+ "Seed removal failed": "",
+ "Seed successfully removed": "",
+ "Segwit (Recommended, cheapest fee)": "",
+ "Segwit (Recommended)": "",
+ "Segwit wrapped (Compatible with old wallets)": "",
+ "Select": "",
+ "Select a field to edit": "",
+ "Select a language to download from the community translation repository.": "",
+ "Select a preset": "",
+ "Select All": "",
+ "Select an existing offering": "",
+ "Select an item to edit": "",
+ "Select default plan": "",
+ "Select existing offering": "",
+ "Select labels": "",
+ "Select Language": "",
+ "Select offering": "",
+ "Select plan": "",
+ "Select store": "",
+ "Select Store": "",
+ "Select the payout method used for refund": "",
+ "Select the store to grant permission for": "",
+ "Select this contribution perk": "",
+ "Select your address type and account": "",
+ "selected": "",
+ "Send": "",
+ "Send {0}": "",
+ "Send a <code>GET</code> request to <code>https://btcpay.example.com/invoices/{invoiceId}</code> with <code>Content-Type: application/json; Authorization: Basic YourLegacyAPIkey\"</code>, Legacy API key can be created with Access Tokens in Store settings": "",
+ "Send invitation email": "",
+ "Send me everything": "",
+ "Send specific events": "",
+ "Send Test Email": "",
+ "Send the email to the buyer, if email was provided to the invoice": "",
+ "Send verification email": "",
+ "Sender's Email Address": "",
+ "Server": "",
+ "Server Emails": "",
+ "Server IPN": "",
+ "Server Name": "",
+ "Server Settings": "",
+ "Server-wide": "",
+ "Service": "",
+ "Services": "",
+ "Set a schedule for automated Lightning Network Payouts.": "",
+ "Set a schedule for automated On-Chain Bitcoin Payouts.": "",
+ "Set as default": "",
+ "Set Password": "",
+ "Set the translation to match the string in the fallback.": "",
+ "Set to default": "",
+ "Set to default settings": "",
+ "Set up a Lightning node": "",
+ "Set up a wallet": "",
+ "Set your password": "",
+ "Settings": "",
+ "Settings saved": "",
+ "Settings updated successfully": "",
+ "Settled": "",
+ "Settled Late": "",
+ "Settled Over": "",
+ "Settled Partial": "",
+ "Setup {0} Wallet": "",
+ "Setup Boltcard": "",
+ "Setup new wallet": "",
+ "Shop Name": "",
+ "Shopify": "",
+ "Show \"Pay in wallet\" button": "",
+ "Show a timer … minutes before invoice expiration": "",
+ "Show all": "",
+ "Show coin selection": "",
+ "Show Confidential QR Code": "",
+ "Show current info": "",
+ "Show export QR": "",
+ "Show multi-sig examples": "",
+ "Show plugins in pre-release": "",
+ "Show QR": "",
+ "Show QR Code": "",
+ "Show QR for wallet camera": "",
+ "Show raw versions": "",
+ "Show recommended fee": "",
+ "Show selected only": "",
+ "Show the payment list in the public receipt page": "",
+ "Show the QR code of the receipt in the public receipt page": "",
+ "Show the store header": "",
+ "Show unconfirmed coins": "",
+ "Show update info": "",
+ "Showing {0} – {1} of {2}": "",
+ "Showing {0} of {1}": "",
+ "Sign": "",
+ "Sign in": "",
+ "Sign PSBT": "",
+ "Sign the transaction": "",
+ "Sign transaction": "",
+ "Sign using our Vault application": "",
+ "Signed out": "",
+ "SIN": "",
+ "Slider": "",
+ "SMTP Server": "",
+ "Softcap Goal": "",
+ "Sold out": "",
+ "Some of the files were not found": "",
+ "Some plugins were disabled due to fatal errors. They may be incompatible with this version of BTCPay Server.": "",
+ "Sort by": "",
+ "Sort by date ascending...": "",
+ "Sort by date descending...": "",
+ "Sort by name ascending...": "",
+ "Sort by name descending...": "",
+ "Sort contribution perks by popularity": "",
+ "Sound": "",
+ "Sounds to play when a payment is made. One sound per line": "",
+ "Source": "",
+ "Sources": "",
+ "Specify additional query string parameters that should be appended to the checkout page once the invoice is created. For example, <code>lang=da-DK</code> would load the checkout page in Danish by default.": "",
+ "Specify the amount and currency for the refund": "",
+ "SSH services are used by the maintenance operations.": "",
+ "SSH settings": "",
+ "Standalone mode, which can be used to generate invoices independent of payment requests or apps.": "",
+ "Start": "",
+ "Start date": "",
+ "Start Date": "",
+ "Start scan": "",
+ "Starter plan monthly cost": "",
+ "Starting index": "",
+ "Starts {0}": "",
+ "Status": "",
+ "Step": "",
+ "Still due": "",
+ "Stop Shopify calls and clear credentials": "",
+ "Storage Provider": "",
+ "Storage settings updated successfully": "",
+ "Store": "",
+ "Store emails settings are now using the server's SMTP settings.": "",
+ "Store emails settings copied from server settings": "",
+ "Store has not enabled Pay Button": "",
+ "Store Id": "",
+ "Store Name": "",
+ "Store Overview": "",
+ "Store removed successfully": "",
+ "Store Settings": "",
+ "Store successfully created": "",
+ "Store successfully deleted.": "",
+ "Store successfully updated": "",
+ "Store template created from store '{0}'. New stores will inherit these settings.": "",
+ "Store template successfully unset": "",
+ "Store Users": "",
+ "Store Website": "",
+ "Store-based permissions will be applied for": "",
+ "Store-level": "",
+ "Store: {0}": "",
+ "Stores": "",
+ "Subject": "",
+ "Submit": "",
+ "Subscriber": "",
+ "Subscriber '{0}' successfully created": "",
+ "Subscriber {0} has been charged": "",
+ "Subscriber {0} has been credited": "",
+ "Subscriber {0} is now {1}": "",
+ "Subscriber {0} is now suspended": "",
+ "Subscriber {0} is now unsuspended": "",
+ "Subscribers": "",
+ "Subscriptions": "",
+ "Subtotal": "",
+ "Subtract fees from amount": "",
+ "Success redirect url": "",
+ "Successfully planned a redelivery": "",
+ "Support URL": "",
+ "Supported by BlueWallet, Cobo Vault, Passport and Specter DIY": "",
+ "Supported Transaction Currencies": "",
+ "Suspend": "",
+ "Suspend Access": "",
+ "Suspend Subscriber": "",
+ "Suspended": "",
+ "Suspension Reason": "",
+ "Switch date format": "",
+ "Syntax error": "",
+ "System": "",
+ "Taproot": "",
+ "Taproot (For advanced users)": "",
+ "Target Amount": "",
+ "Tax": "",
+ "Tax rate": "",
+ "Taxes": "",
+ "Template": "",
+ "Template JSON": "",
+ "Templates": "",
+ "Test": "",
+ "Test connection": "",
+ "Test Email": "",
+ "Test Results:": "",
+ "Testing": "",
+ "Text": "",
+ "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": "",
+ "Thank you for your purchase. Here is your receipt": "",
+ "Thank you!": "",
+ "The {0} offers programmatic access to your instance. You can manage your BTCPay Server (e.g. stores, invoices, users) as well as automate workflows and integrations (see {1}). For that you need the API keys, which can be generated here. Find more information in the {2}.": "",
+ "The <code>macaroon</code> parameter expects the HEX value, it can be obtained using this\n command:": "",
+ "The access key of the service is not set": "",
+ "The access token will be revoked. Do you wish to continue?": "",
+ "The admin {0} will be permanently deleted. This action will also delete all accounts, users and data associated with the server account. Are you sure?": "",
+ "The amount should be more than zero": "",
+ "The app <strong>{0}</strong> and its settings will be permanently deleted.": "",
+ "The app <strong>{0}</strong> and its settings will be permanently deleted. Are you sure?": "",
+ "The app has been archived and will no longer appear in the apps list by default.": "",
+ "The app has been unarchived and will appear in the apps list by default again.": "",
+ "The batch size make sure the scan do not consume too much RAM at once by rescanning several time with smaller subset of addresses.": "",
+ "The brand color needs to be a valid hex color code": "",
+ "The card is now configured": "",
+ "The change output is too small to pay for additional fee.": "",
+ "The change output is too small to pay for additional fee. (Missing {0} BTC)": "",
+ "The chosen field's selected value will be copied to this field upon submission.": "",
+ "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:": "",
+ "The configured name means the value of this field will adjust the invoice amount by multiplying it for public forms and the point of sale app.": "",
+ "The configured name means the value of this field will adjust the invoice amount for public forms and the point of sale app.": "",
+ "The configured name means the value of this field will determine the invoice amount for public forms.": "",
+ "The configured name means the value of this field will determine the invoice currency for public forms.": "",
+ "The credentials and server address are shown as a <code>lndhub://</code> URL on the\n \"Export/Backup\" screen in BlueWallet.": "",
+ "The crowdfund will be visible to anyone.": "",
+ "The crypto currency amount that was overpaid.": "",
+ "The crypto currency price, at the current rate.": "",
+ "The crypto currency price, at the rate the invoice got paid.": "",
+ "The currency to generate the invoice in when generated through this lightning address": "",
+ "The custom color, logo and CSS are applied on the public/customer-facing pages (Invoice, Payment Request, Pull Payment, etc.). The brand color is used as the accent color for buttons, links, etc. It might get adapted to fit the light/dark color scheme.": "",
+ "The device has not been initialized.": "",
+ "The DNS record has been refreshed:": "",
+ "The Dynamic DNS has been successfully queried, your configuration is saved": "",
+ "The Dynamic DNS service has been disabled": "",
+ "The email address is already in use with an other account.": "",
+ "The Email settings have not been configured on this server or store yet. Setting this field will not send emails until then.": "",
+ "The endpoint receiving the payload must validate the payload by checking that the HTTP header <code>BTCPAY-SIG</code> of the callback matches the HMAC256 of the secret on the payload's body bytes.": "",
+ "The file needs to be an image": "",
+ "The file size should be less than 0.5MB": "",
+ "The following methods assume that you already have an existing wallet created and backed up.": "",
+ "The full node is not synched": "",
+ "The full node is synched": "",
+ "The hardware wallet requires previous transactions in the PSBT. Please go to your wallet settings and enable \"Include non-witness UTXO in PSBTs": "",
+ "The invoice currency, at the rate when the refund will be sent.": "",
+ "The invoice has been archived and will no longer appear in the invoice list by default.": "",
+ "The invoice has been unarchived and will appear in the invoice list by default again.": "",
+ "The label {0} will be deleted from all payment requests.": "",
+ "The label {0} will be removed from this wallet and its associated transactions.": "",
+ "The label could not be deleted.": "",
+ "The label could not be renamed.": "",
+ "The label has been successfully deleted.": "",
+ "The label has been successfully renamed.": "",
+ "The Lightning node could not be registered.": "",
+ "The LND seed backup is useful to recover on-chain funds of your LND wallet in case of a corruption of your server.": "",
+ "The name of the field in the invoice's metadata.": "",
+ "The name should be maximum 50 characters.": "",
+ "The node is offline": "",
+ "The node is starting...": "",
+ "The node is synchronized (Height: {0})": "",
+ "The old docker images will be cleaned soon...": "",
+ "The password is the <code>http-password</code> generated in your <code>~/.phoenix/phoenix.conf</code> file.": "",
+ "The path to the LND data directory may vary, the following examples assume <code>/root/.lnd</code>.": "",
+ "The payjoin receiver could not complete the payjoin: {0}": "",
+ "The payment request has been archived and will no longer appear in the payment request list by default again.": "",
+ "The payment request has been unarchived and will appear in the payment request list by default.": "",
+ "The plan has been started.": "",
+ "The plan has been started. ({0} has been refunded)": "",
+ "The plugin could not be downloaded. Try again later.": "",
+ "The preferred link to this server. This is typically used when BTCPay Server is generating an email containing a link which should point to this server.": "",
+ "The previous scan completed and found <b>{0}</b> UTXOs in <b>{1}</b> (The total UTXO set size is {2})": "",
+ "The previous scan stopped with an error:": "",
+ "The provided BIP21 payment URI was malformed": "",
+ "The rates of those currencies, in addition to the default currency, will be recorded when a new invoice is created. The rates will then be accessible through reports.": "",
+ "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 role of {0} has been changed to {1}.": "",
+ "The script language is composed of several rules composed of a currency pair and a mathematical expression. The\n example below will use <code>kraken</code> for both <code>LTC_USD</code> and <code>BTC_USD</code> pairs.": "",
+ "The security device could not be registered.": "",
+ "The security device was registered successfully.": "",
+ "The security device was removed successfully.": "",
+ "The seed was not found": "",
+ "The selected fee rate is too small. The minimum is {0} sat/byte": "",
+ "The server might restart soon if an update is available... (this page won't reload automatically)": "",
+ "The specified amount with the specified currency, at the rate when the refund will be sent.": "",
+ "The store <strong>{0}</strong> will be permanently deleted. This action will also delete all invoices, apps and data associated with the store.": "",
+ "The store template sets defaults for all new stores. It includes rates settings, default invoice settings, checkout display settings but excludes sensitive data like access tokens, payment method settings, webhooks.": "",
+ "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store.": "",
+ "The store will be permanently deleted. This action will also delete all invoices, apps and data associated with the store. Are you sure?": "",
+ "The trial has started.": "",
+ "The uploaded file needs to be a CSS file": "",
+ "The uploaded file needs to be an image": "",
+ "The uploaded file should be less than {0}": "",
+ "The uploaded sound file needs to be an audio file": "",
+ "The uploaded sound file should be less than {0}": "",
+ "The URL to post purchase data.": "",
+ "The user could not be added: {0}": "",
+ "The user could not be invited: {0}": "",
+ "The user could not be updated: {0}": "",
+ "The user declined access to the vault.": "",
+ "The user has been added successfully.": "",
+ "The user has been updated successfully.": "",
+ "The user will be permanently deleted. This action will also delete all stores, invoices, apps and data associated with your user.": "",
+ "The user will not be able to change the field's value": "",
+ "The values being mirrored from another field will be mapped to another value if configured.": "",
+ "The wallet has been successfully pruned ({0} transactions have been removed from the history)": "",
+ "The wallet is already pruned": "",
+ "The webhook has been created": "",
+ "The webhook has been updated": "",
+ "Theme": "",
+ "There are many subscribers, use search to look for them.": "",
+ "There are no {0} pull payments yet.": "",
+ "There are no apps yet.": "",
+ "There are no associated permissions to the API key being requested by the application. The application cannot do anything with your BTCPay Server account other than validating your account exists.": "",
+ "There are no custom labels yet. You can create custom labels by assigning them to your {0}.": "",
+ "There are no dynamic DNS services yet.": "",
+ "There are no email rules for this offering.": "",
+ "There are no files yet.": "",
+ "There are no forms yet.": "",
+ "There are no invoices matching your criteria.": "",
+ "There are no items in your cart yet.": "",
+ "There are no Lightning Addresses yet.": "",
+ "There are no notifications.": "",
+ "There are no payment methods available to provide refunds with for this invoice.": "",
+ "There are no payment requests matching your criteria.": "",
+ "There are no payouts matching these criteria.": "",
+ "There are no processors available.": "",
+ "There are no recent invoices.": "",
+ "There are no recent transactions.": "",
+ "There are no rules yet.": "",
+ "There are no stores yet.": "",
+ "There are no subscribers.": "",
+ "There are no subscription plans.": "",
+ "There are no wallets yet. You can add wallets in the store setup.": "",
+ "There are no webhooks yet.": "",
+ "There isn't any UTXO available to bump fee with CPFP": "",
+ "There was an error generating your wallet: {0}": "",
+ "These translations are maintained by the community at": "",
+ "This account has been locked out because of multiple invalid login attempts. Please try again later.": "",
+ "This account has been locked out. Please try again": "",
+ "This action is permanent and will remove the ability to change the SSH settings via the BTCPay Server user interface.": "",
+ "This action will also delete all stores, invoices, apps and data associated with the user.": "",
+ "This action will delete your rate script. Are you sure to turn off rate rules scripting?": "",
+ "This action will modify your current rate sources. Are you sure to turn on rate rules scripting? (Advanced users)": "",
+ "This action will permanently delete your LND seed and password. You will not be able to recover them if you don't have a backup.": "",
+ "This action will permanently delete your LND seed and password. You will not be able to recover them if you don't have a backup. Are you sure?": "",
+ "This action will prevent {0} from accessing this store and its settings.": "",
+ "This action will prevent the user from accessing this store and its settings. Are you sure?": "",
+ "This action will remove the plan <b>{0}</b>.": "",
+ "This action will remove the rule with the trigger <b>{0}</b>.": "",
+ "This action will remove this plan. Are you sure?": "",
+ "This action will remove this rule. Are you sure?": "",
+ "This app will be removed from this store.": "",
+ "This can be overridden at the Store level.": "",
+ "This card is already configured for another issuer": "",
+ "This card is already in a factory state": "",
+ "This card is already properly configured": "",
+ "This crowdfund page is not publicly viewable!": "",
+ "This device already signed PSBT.": "",
+ "This device can't sign the transaction. (The wallet keypath in your wallet settings seems incorrect)": "",
+ "This dictionary will be removed from this server.": "",
+ "This feature is disabled": "",
+ "This feature is only available to BTC wallets": "",
+ "This full node does not support rescan of the UTXO set": "",
+ "This full node support rescan of the UTXO set": "",
+ "this guide": "",
+ "This invoice got overpaid.": "",
+ "This invoice has been paid": "",
+ "This invoice has expired": "",
+ "This is an extremely dangerous operation!": "",
+ "This is an overpayment of the initial amount.": "",
+ "This is the only admin, so their role can't be removed until another admin is added.": "",
+ "This key, also called \"xpub\", is used to generate individual destination addresses for your invoices.": "",
+ "This label cannot be deleted.": "",
+ "This label cannot be renamed.": "",
+ "This label will be deleted from all payment requests.": "",
+ "This label will be deleted from this wallet and its associated transactions.": "",
+ "This Lightning Address will be removed.": "",
+ "this link": "",
+ "This offering will be removed from this store.": "",
+ "This page exposes information to connect remotely to your full node via the P2P protocol.": "",
+ "This page exposes information to connect remotely to your full node via the RPC protocol.": "",
+ "This page exposes information to use the configured BTCPay Server Configurator to modify this setup.": "",
+ "This page is served in non-secure context (HTTPS, localhost or file://)": "",
+ "This payment method requires javascript.": "",
+ "This permission is not available for your account.": "",
+ "This process disables 2FA until you verify your authenticator app and will also reset your 2FA recovery codes. If you do not complete your authenticator app configuration you may lose access to your account.": "",
+ "This processor cannot handle {0}.": "",
+ "This pull payment does not exists": "",
+ "This QR Code is only valid for 10 minutes": "",
+ "This rate can also be overridden per item.": "",
+ "This store is ready to accept transactions, good job!": "",
+ "This store will still be accessible to users sharing it": "",
+ "This transaction can't be RBF'd": "",
+ "This transaction will change your balance:": "",
+ "This version of NBXplorer is not compatible. Please update to 2.5.22 or above": "",
+ "This webhook will be removed from this store.": "",
+ "This will approve the user <strong>{0}</strong>.": "",
+ "This will send a verification email to <strong>{0}</strong>.": "",
+ "This will send a verification email to the user.": "",
+ "This will send notification mails to the recipient, as configured by the <a href=\"{0}\">email rules</a>.": "",
+ "Those initial settings can be modified later.": "",
+ "Threshold": "",
+ "Time must be at least 1": "",
+ "Timestamp": "",
+ "Tip": "",
+ "Tip percentage amounts (comma separated)": "",
+ "Tips": "",
+ "Title": "",
+ "TLS certificate security\n checks": "",
+ "To": "",
+ "To create a {0} app, you need to <a href='{1}' class='alert-link'>set up a wallet</a> first": "",
+ "To create a {0} app, you need to set up a wallet first": "",
+ "To create a payment request, you need to <a href='{0}'>setup a wallet</a> first": "",
+ "To create a payment request, you need to set up a wallet first": "",
+ "To create an invoice, you need to <a href='{0}'>setup a wallet</a> first": "",
+ "To create an invoice, you need to setup a wallet first": "",
+ "To disable notification for a feature, kindly toggle off the specified feature.": "",
+ "To generate Greenfield API keys, please": "",
+ "To start accepting payments, set up a store.": "",
+ "To start accepting payments, set up a wallet or a Lightning node.": "",
+ "To start using Pay Button, you need to enable this feature explicitly. Once you do so, anyone could create an invoice on your store (via API, for example).": "",
+ "To test your settings, enter an email address": "",
+ "To use an authenticator app go through the following steps:": "",
+ "To use the translation from this dictionary's fallback, you can:": "",
+ "Toggle passphrase visibility": "",
+ "Toggle password visibility": "",
+ "Toggle seed visibility": "",
+ "Token": "",
+ "Token Information": "",
+ "Top Items": "",
+ "Top Perks": "",
+ "Total": "",
+ "Total Balance": "",
+ "Total Contributions": "",
+ "Total due": "",
+ "Total Sales": "",
+ "Transaction": "",
+ "Transaction broadcasted successfully ({0})": "",
+ "Transaction fee": "",
+ "Transaction fee rate:": "",
+ "Transaction Id": "",
+ "Transaction signed successfully, proceeding to review...": "",
+ "Transaction successfully signed": "",
+ "transactions": "",
+ "Transactions": "",
+ "Translations": "",
+ "Translations are formatted as JSON; for example, <b>{0}</b> translates <b>{1}</b> to <b>{2}</b>.": "",
+ "Trial": "",
+ "Trial Period": "",
+ "Trial Period (days)": "",
+ "Trigger": "",
+ "Turning on monetization creates a paid plan for your server.\n All newly registered users will be added to this plan.\n You may also migrate your existing users.\n Only users with an active subscription with access feature can log in and use your server.": "",
+ "Two Factor Authentication": "",
+ "Two-Factor Authentication": "",
+ "Two-Factor Authentication (2FA) is an additional measure to protect your account. In addition to your password you will be asked for a second proof on login. This can be provided by an app (such as Google or Microsoft Authenticator) or a security device (like a Yubikey or your hardware wallet supporting FIDO2).": "",
+ "txid, amount, comment, label": "",
+ "Type": "",
+ "Unable to create the replacement transaction ({0})": "",
+ "Unapprove": "",
+ "Unarchive": "",
+ "Unarchive this app": "",
+ "Unarchive this invoice": "",
+ "Unarchive this payment request": "",
+ "Unarchive this store": "",
+ "Unavailable": "",
+ "Unexpected address returned by the device...": "",
+ "Unexpected payjoin error: {0}": "",
+ "Unify on-chain and lightning payment URL/QR code": "",
+ "Uninstall": "",
+ "Uninstall all disabled plugins": "",
+ "Unknown": "",
+ "Unknown command": "",
+ "Unknown username": "",
+ "Unmark test account": "",
+ "Unnamed Store": "",
+ "Unread": "",
+ "Unsupported exchange": "",
+ "Unsupported hardware wallet, try to update BTCPay Server Vault": "",
+ "Unsuspend Access": "",
+ "Unusual": "",
+ "Update": "",
+ "Update Crowdfund": "",
+ "Update Password": "",
+ "Update Point of Sale": "",
+ "Update Role": "",
+ "Update to the latest version of BTCPay Server.": "",
+ "Update Webhook": "",
+ "Update your account": "",
+ "Updated successfully.": "",
+ "Upgrade": "",
+ "Upload": "",
+ "Upload a file exported from your wallet": "",
+ "Upload Plugin": "",
+ "Upload PSBT from file…": "",
+ "Upload the file exported from your wallet.": "",
+ "Uploaded By": "",
+ "Url": "",
+ "URL": "",
+ "Url of the Dynamic DNS service you are using": "",
+ "Use a simple input style": "",
+ "Use App As Endpoint": "",
+ "use case examples": "",
+ "Use current URL": "",
+ "Use custom node": "",
+ "Use custom SMTP settings for this store": "",
+ "Use custom theme": "",
+ "Use default pay button endpoint": "",
+ "Use Incognito mode or the Tor Browser to ensure no malicious browser plugins are running that could steal your key.": "",
+ "Use internal node": "",
+ "Use Modal": "",
+ "Use PSBT": "",
+ "Use SSL": "",
+ "Use the default Light or Dark Themes, or provide a custom CSS theme file below.": "",
+ "Use the store’s default": "",
+ "User": "",
+ "User approved": "",
+ "User can input custom amount": "",
+ "User can input discount in %": "",
+ "User deleted": "",
+ "User disabled": "",
+ "User enabled": "",
+ "User is admin": "",
+ "User is approved": "",
+ "User not found": "",
+ "User not found or invalid password": "",
+ "User removed successfully.": "",
+ "User successfully updated": "",
+ "User unapproved": "",
+ "User Updates": "",
+ "User was the last enabled admin and could not be disabled.": "",
+ "user@example.com": "",
+ "Username could not be removed": "",
+ "Username is already taken": "",
+ "Users": "",
+ "Uses the store's default currency ({0}) if empty.": "",
+ "Using an HD private key or mnemonic seed": "",
+ "Using apps such as Google or Microsoft Authenticator.": "",
+ "Using BTCPay Server Vault": "",
+ "Using BTCPay Server Vault (NFC)": "",
+ "Using the BTCPay Server internal node for this store requires no further configuration. Click the save\n button below to start accepting Bitcoin through the Lightning Network.": "",
+ "Using the payment button for e-commerce integrations is not recommended since order relevant information can be modified by the user. For e-commerce, you should use our {0}. If this store process commercial transactions, we advise you to {1} before using the payment button.": "",
+ "UTXOs to spend from": "",
+ "Valid for {0} seconds": "",
+ "Validated blocks: {0}": "",
+ "Value": "",
+ "Value Mapper": "",
+ "Value to match": "",
+ "Value to set": "",
+ "Verification Code": "",
+ "Verification email sent": "",
+ "Verification email sent. Please check your email.": "",
+ "Verify": "",
+ "Verify that the <code>orderId</code> is from your backend, that the <code>price</code> is correct and that <code>status</code> is <code>settled</code>": "",
+ "Verifying pin...": "",
+ "via HTTP": "",
+ "via HTTPS": "",
+ "via TCP or unix domain socket connection": "",
+ "via the REST API": "",
+ "View": "",
+ "View All": "",
+ "View all supporters": "",
+ "View Invite": "",
+ "View Reserved Addresses with this label": "",
+ "View seed": "",
+ "Waiting for NFC to be presented...": "",
+ "Wallet": "",
+ "Wallet Balance": "",
+ "Wallet Enabled": "",
+ "Wallet file": "",
+ "Wallet file content": "",
+ "Wallet Labels (BIP-329)": "",
+ "Wallet Recovery Seed": "",
+ "Wallet settings for {0} have been updated.": "",
+ "Wallet's fingerprint fetched.": "",
+ "Wallet's private key is erased from the server. Higher security. To spend, you have to manually input the private key or import it into an external wallet.": "",
+ "Wallet's private key is stored on the server. Spending the funds you received is convenient. To minimize the risk of theft, regularly withdraw funds to a different wallet.": "",
+ "Wallets": "",
+ "Warning: No wallet has been linked to your BTCPay Server Store.": "",
+ "Warning: Payment button should only be used for tips and donations": "",
+ "was paid after expiration": "",
+ "Watch-only wallet": "",
+ "We all forget passwords sometimes. Just provide email address tied to your account, and we'll start the process of helping you recover your account.": "",
+ "We rejected the receiver's payjoin proposal: {0}": "",
+ "We will try to redeliver any failed delivery after 10 seconds, 1 minute and up to 6 times after 10 minutes": "",
+ "Webhook successfully deleted": "",
+ "Webhooks": "",
+ "Webhooks allow BTCPay Server to send HTTP events related to your store to another server.": "",
+ "Welcome to {0}": "",
+ "Well-known Dynamic DNS providers are:": "",
+ "When this is enabled for a hot wallet, you are also able to use the node wallet to spend.": "",
+ "Where to redirect the customer after payment is complete": "",
+ "Which events would you like to trigger this webhook?": "",
+ "Who to send the email to. For multiple emails, separate with a comma.": "",
+ "With <code>DOGE_USD</code> will be expanded to <code>bitpay(DOGE_BTC) * kraken(BTC_USD)</code>. And\n <code>DOGE_CAD</code> will be expanded to <code>bitpay(DOGE_BTC) * ndax(BTC_CAD)</code>.<br />However, we advise you to write it\n that\n way to increase coverage so that <code>DOGE_BTC</code> is also supported:": "",
+ "Would you like to charge <span class=\"subscriber-name fw-semibold\"></span>?": "",
+ "Would you like to credit <span class=\"subscriber-name fw-semibold\"></span>?": "",
+ "Would you like to downgrade <span class=\"subscriber-name fw-semibold\"></span> to <b class=\"changePlanName\"></b> ?": "",
+ "Would you like to invite a new subscriber?": "",
+ "Would you like to proceed with suspending the following user?": "",
+ "Would you like to upgrade <span class=\"subscriber-name fw-semibold\"></span> to <b class=\"changePlanName\"></b>?": "",
+ "Yes": "",
+ "You are not server administrator": "",
+ "You are server administrator": "",
+ "You can also apply filters to your search by searching for <code>filtername:value</code>. Be sure to split your search parameters with comma. Supported filters are:": "",
+ "You can also share the link/LNURL or encode it in a QR code.": "",
+ "You can also use this LNDhub-URL as the connection string and BTCPay Server converts it into the expected\n <code>type=lndhub</code> connection string format:": "",
+ "You can change the domain name of your server by following {0}.": "",
+ "You can decode a PSBT by either pasting its content, uploading the file or scanning the wallet QR code.": "",
+ "You can embed this POS via an iframe.": "",
+ "You can enter here SSH public keys authorized to connect to your server.": "",
+ "You can give this server a custom name, which will appear on public facing pages.": "",
+ "You can host point of sale buttons in an external website with the following code.": "",
+ "You can omit <code>certthumbprint</code> if the certificate is trusted by your machine. The <code>certthumbprint</code> can be\n obtained using this command:": "",
+ "You can only refund an invoice that has been settled. Please wait for the transaction to confirm on the blockchain before attempting to refund it.": "",
+ "You can sign the transaction using one of the following methods.": "",
+ "You can then ship your order": "",
+ "You can use QR Code to connect to {0} with compatible wallets.": "",
+ "You can use QR Code to connect to your {0} from your mobile.": "",
+ "You can use this QR Code to connect external software to your C-Lightning instance.<br />": "",
+ "You can use this QR Code to connect external software to your LND instance. This QR Code is only valid for 10 minutes.": "",
+ "You cannot edit an archived payment request.": "",
+ "You cannot login over an insecure connection. Please use HTTPS or Tor.": "",
+ "You currently have no stores configured.": "",
+ "You do not have a local username/password for this site. Add a local account so you can log in without an external login.": "",
+ "You do not have the permissions to change this settings": "",
+ "You have no recovery codes left.": "",
+ "You have requested to log in with a recovery code. This login will not be remembered until you provide an authenticator app code at login or disable 2FA and log in again.": "",
+ "You have successfully signed out.": "",
+ "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 must have a confirmed email to log in.": "",
+ "You need at least one payout method": "",
+ "You need to configure email settings before this feature works. <a class='alert-link configure-email' href='{0}'>Configure email settings</a>.": "",
+ "You need to connect to a Lightning node before adjusting its settings.": "",
+ "You need to restart BTCPay Server in order to update your active plugins.": "",
+ "You need to select a store before creating an invoice.": "",
+ "You need to select a store first": "",
+ "You need to update your version of NBXplorer": "",
+ "You only have 1 recovery code left.": "",
+ "You really should not type your seed into a device that is connected to the internet.": "",
+ "Your access to BTCPay Server is over an unsecured network. If you are using the docker deployment method with NGINX and HTTPS is not available, you probably did not configure your DNS settings correctly. We disabled the register and login link so you don't leak your credentials.": "",
+ "Your account has been disabled. Please contact server administrator.": "",
+ "Your account will no longer be linked to the lightning node <strong>{0}</strong> as an option for two-factor authentication.": "",
+ "Your account will no longer have the security device <strong>{0}</strong> as an option for two-factor authentication.": "",
+ "Your account will no longer have this Lightning wallet as an option for two-factor authentication.": "",
+ "Your account will no longer have this security device as an option for two-factor authentication.": "",
+ "Your available balance is": "",
+ "Your dynamic DNS hostname": "",
+ "Your email has been confirmed.": "",
+ "Your email has been confirmed. Please set your password.": "",
+ "Your email server has not been configured.": "",
+ "Your existing recovery codes will no longer be valid!": "",
+ "Your instance administrator has disabled the use of the Internal node for non-admin users.": "",
+ "Your node address: {0}": "",
+ "Your password has been changed.": "",
+ "Your password has been set by the user who invited you.": "",
+ "Your password has been set.": "",
+ "Your plan does not allow you to log in.": "",
+ "Your profile has been updated": "",
+ "Your subscription is not active.": "",
+ "Your subscription is suspended. {0}": "",
+ "Your two-factor authenticator app will provide you with a unique code.": "",
+ "Your user account has no password set.": "",
+ "Your user account is currently disabled.": "",
+ "Your user account requires approval by an admin before you can log in.": "",
+ "Your wallet has been generated.": "",
+ "Zero amount invoices are disabled": ""
+}
+""";
+ Default = Translations.CreateFromJson(knownTranslations);
+ Default = new Translations(new KeyValuePair<string, string>[]
+ {
+ // You can add additional hard coded default here
+ // KeyValuePair.Create("key1", "value")
+ // KeyValuePair.Create("key2", "value")
+ }, Default);
+ }
+
+ /// <summary>
+ /// Translations which are already in the Default aren't saved into the database.
+ /// This allows us to automatically update the English version if the translations didn't change.
+ ///
+ /// We only save into a database the key/values that differ from Default
+ /// </summary>
+ public static Translations Default;
+ public static readonly string DefaultLanguage = "English";
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/Translations.Extensions.cs b/BTCPayServer/Plugins/Translations/Translations.Extensions.cs
new file mode 100644
index 0000000..6d0db7b
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Translations.Extensions.cs
@@ -0,0 +1,28 @@
+#nullable enable
+using System.Collections.Generic;
+using System.Linq;
+using BTCPayServer.Payments;
+using BTCPayServer.Plugins.Translations;
+using BTCPayServer.Services;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace BTCPayServer;
+
+// We use a partial class here to avoid breaking existing plugins
+public static partial class Extensions
+{
+ public static IServiceCollection AddDefaultTranslations(this IServiceCollection services, params string[] keyValues)
+ {
+ return services.AddDefaultTranslations(keyValues.Select(k => KeyValuePair.Create<string, string?>(k, string.Empty)).ToArray());
+ }
+ public static IServiceCollection AddDefaultPrettyName(this IServiceCollection services, PaymentMethodId paymentMethodId, string defaultPrettyName)
+ {
+ services.AddSingleton<PrettyNameProvider.UntranslatedPrettyName>(new PrettyNameProvider.UntranslatedPrettyName(paymentMethodId, defaultPrettyName));
+ return services.AddDefaultTranslations(KeyValuePair.Create<string, string?>(PrettyNameProvider.GetTranslationKey(paymentMethodId), defaultPrettyName));
+ }
+ public static IServiceCollection AddDefaultTranslations(this IServiceCollection services, params KeyValuePair<string, string?>[] keyValues)
+ {
+ services.AddSingleton<IDefaultTranslationProvider>(new InMemoryDefaultTranslationProvider(keyValues));
+ return services;
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/Translations.cs b/BTCPayServer/Plugins/Translations/Translations.cs
new file mode 100644
index 0000000..2ea6ffd
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Translations.cs
@@ -0,0 +1,121 @@
+#nullable enable
+using System.Collections;
+using System.Collections.Frozen;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Newtonsoft.Json.Linq;
+
+namespace BTCPayServer.Plugins.Translations
+{
+ public partial class Translations : IEnumerable<KeyValuePair<string, string>>
+ {
+ public record Diff(string Key)
+ {
+ public record Deleted(string Key, string OldValue) : Diff(Key);
+ public record Added(string Key, string Value) : Diff(Key);
+ public record Modified(string Key, string NewValue, string OldValue) : Diff(Key);
+ }
+ public static bool TryCreateFromJson(string text, [MaybeNullWhen(false)] out Translations translations)
+ {
+ translations = null;
+ try
+ {
+ translations = CreateFromJson(text);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public static Translations CreateFromJson(string text)
+ {
+ text = (text ?? "{}");
+ var translations = new List<(string key, string? value)>();
+ foreach (var prop in JObject.Parse(text).Properties())
+ {
+ var v = prop.Value.Value<string>();
+ if (string.IsNullOrEmpty(v))
+ translations.Add((prop.Name, prop.Name));
+ else
+ translations.Add((prop.Name, v));
+ }
+ return new Translations(translations
+ .Select(t => KeyValuePair.Create(t.key, t.value)));
+ }
+
+ public Translations(IEnumerable<KeyValuePair<string, string?>> records) : this (records, null)
+ {
+ }
+ public Translations(IEnumerable<KeyValuePair<string, string?>> records, Translations? fallback)
+ {
+ Dictionary<string, string> thisRecords = new Dictionary<string, string>();
+ foreach (var r in records)
+ {
+ var v = r.Value?.Trim();
+ if (string.IsNullOrEmpty(v))
+ continue;
+ thisRecords.TryAdd(r.Key.Trim(), v);
+ }
+ if (fallback is not null)
+ {
+ foreach (var r in fallback.Records)
+ {
+ thisRecords.TryAdd(r.Key, r.Value);
+ }
+ }
+ Records = thisRecords.ToFrozenDictionary();
+ }
+ public readonly FrozenDictionary<string, string> Records;
+
+ public string? this[string? key] => key is null ? null : Records.TryGetValue(key, out var v) ? v : null;
+
+ public Diff[] CalculateDiff(Translations translations)
+ {
+ List<Diff> diff = new List<Diff>(translations.Records.Count + 10);
+ foreach (var kv in translations)
+ {
+ if (Records.TryGetValue(kv.Key, out var oldValue))
+ {
+ if (oldValue != kv.Value)
+ diff.Add(new Diff.Modified(kv.Key, kv.Value, oldValue));
+ }
+ else
+ {
+ diff.Add(new Diff.Added(kv.Key, kv.Value));
+ }
+ }
+ foreach (var kv in this)
+ {
+ if (!translations.Records.ContainsKey(kv.Key))
+ diff.Add(new Diff.Deleted(kv.Key, kv.Value));
+ }
+ return diff.ToArray();
+ }
+
+ public Translations WithFallback(Translations? fallback)
+ {
+ return new Translations(this!, fallback);
+ }
+ public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
+ {
+ return Records.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ public string ToJsonFormat()
+ {
+ JObject obj = new JObject();
+ foreach (var record in Records)
+ {
+ obj.Add(record.Key, record.Value);
+ }
+ return obj.ToString(Newtonsoft.Json.Formatting.Indented);
+ }
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/TranslationsPlugin.cs b/BTCPayServer/Plugins/Translations/TranslationsPlugin.cs
new file mode 100644
index 0000000..3c68162
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/TranslationsPlugin.cs
@@ -0,0 +1,27 @@
+using BTCPayServer.Abstractions.Extensions;
+using BTCPayServer.Abstractions.Models;
+using Microsoft.AspNetCore.Mvc.Localization;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Localization;
+
+namespace BTCPayServer.Plugins.Translations;
+
+public class TranslationsPlugin : BaseBTCPayServerPlugin
+{
+ public const string Area = "Translations";
+ public override string Identifier => "BTCPayServer.Plugins.Translations";
+ public override string Name => "Translations";
+ public override string Description => "Allows you to translate BTCPay Server backend";
+
+ public override void Execute(IServiceCollection services)
+ {
+ services.TryAddSingleton<ViewLocalizer>();
+ services.TryAddSingleton<IStringLocalizerFactory, LocalizerFactory>();
+ services.TryAddSingleton<IHtmlLocalizerFactory, LocalizerFactory>();
+ services.TryAddSingleton<LocalizerService>();
+ services.TryAddSingleton<LanguagePackUpdateService>();
+ services.AddStartupTask<LoadTranslationsStartupTask>();
+ services.TryAddSingleton<IStringLocalizer>(o => o.GetRequiredService<IStringLocalizerFactory>().Create("", ""));
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/Views/CreateDictionary.cshtml b/BTCPayServer/Plugins/Translations/Views/CreateDictionary.cshtml
new file mode 100644
index 0000000..e49d7ff
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Views/CreateDictionary.cshtml
@@ -0,0 +1,36 @@
+@using BTCPayServer.Views.Server
+@model CreateDictionaryViewModel
+@{
+ ViewData.SetLayoutModel(new LayoutModel(nameof(ServerNavPages.Translations), "Create a new dictionary")
+ .SetCategory(WellKnownCategories.Server));
+}
+<form method="post">
+ <div class="sticky-header">
+ <nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">
+ <a asp-action="ListDictionaries" text-translate="true">Dictionaries</a>
+ </li>
+ <li class="breadcrumb-item active" aria-current="page">@ViewData["Title"]</li>
+ </ol>
+ <h2>@ViewData["Title"]</h2>
+ </nav>
+ <input id="page-primary" type="submit" value="Create" class="btn btn-primary" />
+ </div>
+
+ <div class="row">
+ <div class="col-xl-8 col-xxl-constrain">
+ <div class="form-group">
+ <label asp-for="Name" class="form-label" text-translate="true"></label>
+ <input asp-for="Name" class="form-control" />
+ <span asp-validation-for="Name" class="text-danger"></span>
+ </div>
+ <div class="form-group">
+ <label asp-for="Fallback" class="form-label" text-translate="true"></label>
+ <select asp-for="Fallback" class="form-select w-auto" asp-items="@Model.DictionariesListItems"></select>
+ <span asp-validation-for="Fallback" class="text-danger"></span>
+ <div class="form-text" text-translate="true">If a translation isn’t available in the new dictionary, it will be searched in the fallback.</div>
+ </div>
+ </div>
+ </div>
+</form>
diff --git a/BTCPayServer/Plugins/Translations/Views/CreateDictionaryViewModel.cs b/BTCPayServer/Plugins/Translations/Views/CreateDictionaryViewModel.cs
new file mode 100644
index 0000000..b3c98fd
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Views/CreateDictionaryViewModel.cs
@@ -0,0 +1,21 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+using BTCPayServer.Services;
+using Microsoft.AspNetCore.Mvc.Rendering;
+
+namespace BTCPayServer.Plugins.Translations.Views;
+public class CreateDictionaryViewModel
+{
+ [Required(AllowEmptyStrings = false)]
+ public string Name { get; set; }
+ public string Fallback { get; set; }
+ public SelectListItem[] DictionariesListItems { get; set; }
+
+ internal CreateDictionaryViewModel SetDictionaries(LocalizerService.Dictionary[] dictionaries)
+ {
+ var items = dictionaries.Select(d => new SelectListItem(d.DictionaryName, d.DictionaryName, d.DictionaryName == Fallback)).ToArray();
+ DictionariesListItems = items;
+ return this;
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/Views/EditDictionary.cshtml b/BTCPayServer/Plugins/Translations/Views/EditDictionary.cshtml
new file mode 100644
index 0000000..645c9b4
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Views/EditDictionary.cshtml
@@ -0,0 +1,49 @@
+@using BTCPayServer.Views.Server
+@model EditDictionaryViewModel
+@{
+ ViewData.SetLayoutModel(new LayoutModel(nameof(ServerNavPages.Translations), Context.GetRouteValue("dictionary") as string)
+ .SetCategory(WellKnownCategories.Server));
+}
+
+<form method="post" class="d-flex flex-column">
+ <div class="sticky-header">
+ <nav aria-label="breadcrumb">
+ <ol class="breadcrumb">
+ <li class="breadcrumb-item">
+ <a asp-action="ListDictionaries" text-translate="true">Dictionaries</a>
+ </li>
+ <li class="breadcrumb-item active" aria-current="page" text-translate="true">@ViewData["Title"]</li>
+ </ol>
+ <h2 text-translate="true">@ViewData["Title"]</h2>
+ </nav>
+ <div>
+ <button cheat-mode="true" class="btn btn-outline-info" name="command" value="Fake" text-translate="true">Fill fake</button>
+ <button id="page-primary" type="submit" class="btn btn-primary" name="command" value="Save" text-translate="true">Save</button>
+ </div>
+ </div>
+ <partial name="_StatusMessage" />
+
+ <div class="d-flex mb-4">
+ <div class="flex-fill">
+ <p>@ViewLocalizer["Translations are formatted as JSON; for example, <b>{0}</b> translates <b>{1}</b> to <b>{2}</b>.", "{ \"Welcome\": \"Bienvenue\" }", "Welcome", "Bienvenue"]</p>
+ <p class="mb-0" text-translate="true">
+ To use the translation from this dictionary's fallback, you can:
+ </p>
+ <ul>
+ <li text-translate="true">Remove the translation from this dictionary.</li>
+ <li text-translate="true">Set the translation to match the string in the fallback.</li>
+ </ul>
+ <p class="mb-0" text-translate="true">Please note that not all text is translatable, and future updates may modify existing translations or introduce new translatable phrases.</p>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-xl-8 col-xxl-constrain d-flex flex-column">
+ <div class="form-group">
+ <label asp-for="Translations" class="form-label"></label>
+ <textarea asp-for="Translations" class="form-control translation-editor" rows="@Model.Lines" cols="40"></textarea>
+ <span asp-validation-for="Translations" class="text-danger"></span>
+ </div>
+ </div>
+ </div>
+</form>
diff --git a/BTCPayServer/Plugins/Translations/Views/EditDictionaryViewModel.cs b/BTCPayServer/Plugins/Translations/Views/EditDictionaryViewModel.cs
new file mode 100644
index 0000000..a079bb1
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Views/EditDictionaryViewModel.cs
@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace BTCPayServer.Plugins.Translations.Views;
+
+public class EditDictionaryViewModel
+{
+ [Display(Name = "Translations")]
+ public string Translations { get; set; }
+ public int Lines { get; set; }
+ public string Command { get; set; }
+
+ internal EditDictionaryViewModel SetTranslations(Translations translations)
+ {
+ Translations = translations.ToJsonFormat();
+ Lines = translations.Records.Count;
+ return this;
+ }
+}
diff --git a/BTCPayServer/Plugins/Translations/Views/ListDictionaries.cshtml b/BTCPayServer/Plugins/Translations/Views/ListDictionaries.cshtml
new file mode 100644
index 0000000..b1cad03
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Views/ListDictionaries.cshtml
@@ -0,0 +1,121 @@
+@using BTCPayServer.Views.Server
+@model ListDictionariesViewModel
+@{
+ ViewData.SetLayoutModel(new LayoutModel(nameof(ServerNavPages.Translations), StringLocalizer["Dictionaries"])
+ .SetCategory(WellKnownCategories.Server));
+}
+
+<div class="sticky-header">
+ <h2>@ViewData["Title"]</h2>
+ <div class="d-flex gap-2">
+ <button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#downloadLanguagePackModal" text-translate="true">
+ Download language pack
+ </button>
+ <a id="page-primary" asp-action="CreateDictionary" class="btn btn-primary" role="button" text-translate="true">
+ Create
+ </a>
+ </div>
+</div>
+<partial name="_StatusMessage" />
+
+<p class="mb-0" text-translate="true">
+ Dictionaries enable you to translate the BTCPay Server backend into different languages.
+</p>
+
+<div class="table-responsive">
+ <table class="table table-hover">
+ <thead>
+ <tr>
+ <th text-translate="true">Dictionary</th>
+ <th text-translate="true">Fallback</th>
+ <th class="actions-col"></th>
+ </tr>
+ </thead>
+ <tbody>
+ @foreach (var v in Model.Dictionaries)
+ {
+ <tr>
+ <td>
+ <div class="d-flex flex-wrap align-items-center gap-2">
+ @if (!v.Editable)
+ {
+ <span >@v.DictionaryName</span>
+ }
+ else
+ {
+ <a asp-action="EditDictionary" asp-route-dictionary="@v.DictionaryName">@v.DictionaryName</a>
+ }
+ @if (v.IsSelected)
+ {
+ <span class="badge bg-info" text-translate="true">
+ In use
+ </span>
+ }
+ </div>
+ </td>
+ <td>@v.Fallback</td>
+ <td class="actions-col">
+ <div class="d-inline-flex align-items-center gap-3">
+ @if (v.IsDownloadedLanguagePack && v.UpdateAvailable)
+ {
+ <form method="post" asp-action="DownloadLanguagePack" asp-route-language="@v.DictionaryName" class="d-inline">
+ <button type="submit" class="btn btn-link p-0">Update</button>
+ </form>
+ }
+ <a asp-action="CreateDictionary" asp-route-fallback="@v.DictionaryName" text-translate="true">Clone</a>
+ @if (!v.IsSelected)
+ {
+ <form method="post" asp-action="SelectDictionary" asp-route-dictionary="@v.DictionaryName" class="d-inline">
+ <button id="Select-@v.DictionaryName" type="submit" class="btn btn-link p-0" text-translate="true">Select
+ </button>
+ </form>
+ }
+ @if (v.Editable && !v.IsSelected)
+ {
+ <a id="Delete-@v.DictionaryName" asp-action="DeleteDictionary" asp-route-dictionary="@v.DictionaryName" data-bs-toggle="modal" data-bs-target="#ConfirmModal" data-description="The dictionary <b>@Html.Encode(v.DictionaryName)</b> will be removed from this server." data-confirm-input="@StringLocalizer["Delete"]" text-translate="true">Remove</a>
+ }
+ </div>
+ </td>
+ </tr>
+ }
+ </tbody>
+ </table>
+ <partial name="_Confirm" model="@(new ConfirmModel(StringLocalizer["Delete dictionary"], StringLocalizer["This dictionary will be removed from this server."], StringLocalizer["Delete"]))" />
+</div>
+
+@* Download Language Pack Modal *@
+<div class="modal fade" id="downloadLanguagePackModal" tabindex="-1" role="dialog" aria-labelledby="downloadLanguagePackModalTitle" aria-hidden="true" data-bs-backdrop="static">
+ <div class="modal-dialog modal-dialog-centered" role="document">
+ <form method="post" asp-action="DownloadLanguagePack">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title" id="downloadLanguagePackModalTitle" text-translate="true">Download Language Pack</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
+ <vc:icon symbol="close" />
+ </button>
+ </div>
+ <div class="modal-body">
+ <p text-translate="true">Select a language to download from the community translation repository.</p>
+ <p class="mb-3">
+ <span text-translate="true">These translations are maintained by the community at</span>
+ <a href="https://github.com/btcpayserver/btcpayserver-translator" target="_blank" rel="noopener noreferrer">btcpayserver-translator</a>.
+ </p>
+ <div class="form-group">
+ <label for="language" class="form-label" text-translate="true">Select Language</label>
+ <select id="language" name="language" class="form-select" required>
+ <option value="" text-translate="true">Choose a language...</option>
+ @foreach (var lang in LanguagePackUpdateService.GetDownloadableLanguages())
+ {
+ <option value="@lang">@lang</option>
+ }
+ </select>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" text-translate="true">Cancel</button>
+ <button type="submit" class="btn btn-primary" text-translate="true">Download</button>
+ </div>
+ </div>
+ </form>
+ </div>
+</div>
diff --git a/BTCPayServer/Plugins/Translations/Views/ListDictionariesViewModel.cs b/BTCPayServer/Plugins/Translations/Views/ListDictionariesViewModel.cs
new file mode 100644
index 0000000..94d31a5
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Views/ListDictionariesViewModel.cs
@@ -0,0 +1,19 @@
+using System.Collections.Generic;
+
+namespace BTCPayServer.Plugins.Translations.Views;
+
+public class ListDictionariesViewModel
+{
+ public class DictionaryViewModel
+ {
+ public string DictionaryName { get; set; }
+ public string Fallback { get; set; }
+ public string Source { get; set; }
+ public bool Editable { get; set; }
+ public bool IsSelected { get; set; }
+ public bool IsDownloadedLanguagePack { get; set; }
+ public bool UpdateAvailable { get; set; }
+ }
+
+ public List<DictionaryViewModel> Dictionaries = [];
+}
diff --git a/BTCPayServer/Plugins/Translations/Views/_ViewImports.cshtml b/BTCPayServer/Plugins/Translations/Views/_ViewImports.cshtml
new file mode 100644
index 0000000..1087767
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Views/_ViewImports.cshtml
@@ -0,0 +1,3 @@
+@using BTCPayServer.Plugins.Translations.Views
+@namespace BTCPayServer.Plugins.Translations.Views
+
diff --git a/BTCPayServer/Plugins/Translations/Views/_ViewStart.cshtml b/BTCPayServer/Plugins/Translations/Views/_ViewStart.cshtml
new file mode 100644
index 0000000..a5f1004
--- /dev/null
+++ b/BTCPayServer/Plugins/Translations/Views/_ViewStart.cshtml
@@ -0,0 +1,3 @@
+@{
+ Layout = "_Layout";
+}
diff --git a/BTCPayServer/Services/LanguagePackUpdateService.cs b/BTCPayServer/Services/LanguagePackUpdateService.cs
deleted file mode 100644
index dd179a1..0000000
--- a/BTCPayServer/Services/LanguagePackUpdateService.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Linq;
-using System.Net.Http;
-using System.Threading.Tasks;
-using Newtonsoft.Json.Linq;
-
-namespace BTCPayServer.Services
-{
- public class LanguagePackUpdateService
- {
- private readonly ConcurrentDictionary<string, (bool UpdateAvailable, DateTime CheckedAt)> _updateCheckCache = new();
- private readonly TimeSpan _cacheExpiration = TimeSpan.FromHours(1);
- private readonly IHttpClientFactory _httpClientFactory;
-
- public LanguagePackUpdateService(IHttpClientFactory httpClientFactory)
- {
- _httpClientFactory = httpClientFactory;
- }
-
- public static string[] GetDownloadableLanguages()
- {
- return new[]
- {
- "Dutch",
- "French",
- "German",
- "Hindi",
- "Indonesian",
- "Italian",
- "Japanese",
- "Norwegian",
- "Korean",
- "Portuguese (Brazil)",
- "Russian",
- "Serbian",
- "Spanish",
- "Thai",
- "Turkish"
- };
- }
-
- public async Task<bool> CheckForLanguagePackUpdateCached(string language, JObject metadata)
- {
- var cacheKey = language;
-
- if (_updateCheckCache.TryGetValue(cacheKey, out var cached))
- {
- if (DateTime.UtcNow - cached.CheckedAt < _cacheExpiration)
- {
- return cached.UpdateAvailable;
- }
- }
-
- var updateAvailable = await CheckForLanguagePackUpdate(language, metadata);
- _updateCheckCache[cacheKey] = (updateAvailable, DateTime.UtcNow);
-
- return updateAvailable;
- }
-
- public void InvalidateCache(string language)
- {
- _updateCheckCache.TryRemove(language, out _);
- }
-
- private async Task<bool> CheckForLanguagePackUpdate(string language, JObject metadata)
- {
- try
- {
- if (!GetDownloadableLanguages().Contains(language))
- {
- return false;
- }
-
- var fileName = Uri.EscapeDataString(language.ToLowerInvariant());
- var url = $"https://raw.githubusercontent.com/btcpayserver/btcpayserver-translator/main/translations/{fileName}.json";
-
- var httpClient = _httpClientFactory.CreateClient();
- httpClient.Timeout = TimeSpan.FromSeconds(10);
-
- var remoteContent = await httpClient.GetStringAsync(url);
-
- var remoteHash = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(remoteContent));
- var remoteVersion = Convert.ToHexString(remoteHash);
- var localVersion = metadata["version"]?.ToString();
-
- if (string.IsNullOrEmpty(localVersion))
- return true;
-
- return remoteVersion != localVersion;
- }
- catch (HttpRequestException)
- {
- return false;
- }
- catch (TaskCanceledException)
- {
- return false;
- }
- }
- }
-}
diff --git a/BTCPayServer/Services/LocalizerFactory.cs b/BTCPayServer/Services/LocalizerFactory.cs
deleted file mode 100644
index 349250d..0000000
--- a/BTCPayServer/Services/LocalizerFactory.cs
+++ /dev/null
@@ -1,130 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Xml.Linq;
-using BTCPayServer.Logging;
-using Microsoft.AspNetCore.Mvc.Localization;
-using Microsoft.AspNetCore.Mvc.TagHelpers;
-using Microsoft.Extensions.Localization;
-using Microsoft.Extensions.Logging;
-
-namespace BTCPayServer.Services
-{
- public class LocalizerFactory : IStringLocalizerFactory, IHtmlLocalizerFactory
- {
- internal readonly Logs _logs;
- private readonly LocalizerService _localizerService;
-
- class StringLocalizer : IStringLocalizer, IHtmlLocalizer
- {
- private Type _resourceSource;
- private string _baseName;
- private string _location;
- private LocalizerFactory _Factory;
-
- public StringLocalizer(LocalizerFactory factory, Type resourceSource)
- {
- _Factory = factory;
- _resourceSource = resourceSource;
- }
- Logs Logs => _Factory._logs;
-
-
- public StringLocalizer(LocalizerFactory jsonStringLocalizerFactory, string baseName, string location)
- {
- _Factory = jsonStringLocalizerFactory;
- _baseName = baseName;
- _location = location;
- }
- Translations Translations => _Factory._localizerService.Translations;
- public LocalizedString this[string name]
- {
- get
- {
- //Logs.PayServer.LogInformation($"this[name] with name:{name}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
- Translations.Records.TryGetValue(name, out var result);
- result = result ?? name;
- return new LocalizedString(name, result);
- }
- }
-
- public LocalizedString this[string name, params object[] arguments]
- {
- get
- {
- //var args = String.Join(", ", arguments.Select((a, i) => $"arg[{i}]:{a}").ToArray());
- //Logs.PayServer.LogInformation($"this[name, arguments] with name:{name}, {args}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
- Translations.Records.TryGetValue(name, out var result);
- result = result ?? name;
- return new LocalizedString(name, string.Format(result, arguments));
- }
- }
-
- public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
- {
- //Logs.PayServer.LogInformation($"GetAllStrings");
- return Translations.Records.Select(r => new LocalizedString(r.Key, r.Value));
- }
-
- LocalizedHtmlString IHtmlLocalizer.this[string name]
- {
- get
- {
- //Logs.PayServer.LogInformation($"[HTML]: this[name] with name:{name}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
- Translations.Records.TryGetValue(name, out var result);
- result = result ?? name;
- return new LocalizedHtmlString(name, result);
- }
- }
-
- LocalizedHtmlString IHtmlLocalizer.this[string name, params object[] arguments]
- {
- get
- {
- //var args = String.Join(", ", arguments.Select((a, i) => $"arg[{i}]:{a}").ToArray());
- //Logs.PayServer.LogInformation($"[HTML]:this[name, arguments] with name:{name}, {args}, location:{_location}, baseName:{_baseName}, resource:{_resourceSource}");
- Translations.Records.TryGetValue(name, out var result);
- result = result ?? name;
- return new LocalizedHtmlString(name, result, true, arguments);
- }
- }
-
- public LocalizedString GetString(string name)
- {
- //Logs.PayServer.LogInformation($"[HTML] GetString(name):");
- return this[name];
- }
-
- public LocalizedString GetString(string name, params object[] arguments)
- {
- //var args = String.Join(", ", arguments.Select((a, i) => $"arg[{i}]:{a}").ToArray());
- Logs.PayServer.LogInformation($"[HTML] GetString(name,args):");
- return this[name, arguments];
- }
- }
- public LocalizerFactory(Logs logs, LocalizerService localizerService)
- {
- _logs = logs;
- _localizerService = localizerService;
- }
- public IStringLocalizer Create(Type resourceSource)
- {
- return new StringLocalizer(this, resourceSource);
- }
-
- public IStringLocalizer Create(string baseName, string location)
- {
- return new StringLocalizer(this, baseName, location);
- }
-
- IHtmlLocalizer IHtmlLocalizerFactory.Create(Type resourceSource)
- {
- return new StringLocalizer(this, resourceSource);
- }
-
- IHtmlLocalizer IHtmlLocalizerFactory.Create(string baseName, string location)
- {
- return new StringLocalizer(this, baseName, location);
- }
- }
-}
diff --git a/BTCPayServer/Services/LocalizerService.cs b/BTCPayServer/Services/LocalizerService.cs
deleted file mode 100644
index 30e75e8..0000000
--- a/BTCPayServer/Services/LocalizerService.cs
+++ /dev/null
@@ -1,217 +0,0 @@
-#nullable enable
-using System.Collections;
-using System.Collections.Frozen;
-using Dapper;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Threading.Tasks;
-using BTCPayServer.Data;
-using Microsoft.EntityFrameworkCore;
-using System;
-using System.Security.Cryptography;
-using System.Text;
-using Newtonsoft.Json.Linq;
-using System.Data;
-using System.Data.Common;
-using Microsoft.Extensions.Logging;
-using static BTCPayServer.Services.LocalizerService;
-
-namespace BTCPayServer.Services
-{
- public interface IDefaultTranslationProvider
- {
- Task<KeyValuePair<string, string?>[]> GetDefaultTranslations();
- }
- public class InMemoryDefaultTranslationProvider : IDefaultTranslationProvider
- {
- private readonly KeyValuePair<string, string?>[] _values;
-
- public InMemoryDefaultTranslationProvider(KeyValuePair<string, string?>[] values)
- {
- _values = values;
- }
- public Task<KeyValuePair<string, string?>[]> GetDefaultTranslations()
- {
- return Task.FromResult(_values);
- }
- }
- public class LocalizerService
- {
- public LocalizerService(
- ILogger<LocalizerService> logger,
- ApplicationDbContextFactory contextFactory,
- ISettingsAccessor<PoliciesSettings> settingsAccessor,
- IEnumerable<IDefaultTranslationProvider> defaultTranslationProviders)
- {
- _logger = logger;
- _ContextFactory = contextFactory;
- _settingsAccessor = settingsAccessor;
- _defaultTranslationProviders = defaultTranslationProviders;
- _LoadedTranslations = new LoadedTranslations(Translations.Default, Translations.Default, Translations.DefaultLanguage);
- }
-
- public record LoadedTranslations(Translations Translations, Translations Fallback, string LangName);
- LoadedTranslations _LoadedTranslations;
- public Translations Translations => _LoadedTranslations.Translations;
-
- private readonly ILogger<LocalizerService> _logger;
- private readonly ApplicationDbContextFactory _ContextFactory;
- private readonly ISettingsAccessor<PoliciesSettings> _settingsAccessor;
- private readonly IEnumerable<IDefaultTranslationProvider> _defaultTranslationProviders;
-
- /// <summary>
- /// Load the translation of the server into memory
- /// </summary>
- /// <returns></returns>
- public async Task Load()
- {
- try
- {
- _LoadedTranslations = await GetTranslations(_settingsAccessor.Settings.LangDictionary);
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Failed to load translations");
- throw;
- }
- }
-
- public async Task<LoadedTranslations> GetTranslations(string dictionaryName)
- {
- var ctx = _ContextFactory.CreateContext();
- var conn = ctx.Database.GetDbConnection();
- var all = await conn.QueryAsync<(bool fallback, string sentence, string? translation)>(
- "SELECT 'f'::BOOL fallback, sentence, translation FROM translations WHERE dict_id=@dict_id " +
- "UNION ALL " +
- "SELECT 't'::BOOL fallback, sentence, translation FROM translations WHERE dict_id=(SELECT fallback FROM lang_dictionaries WHERE dict_id=@dict_id)",
- new
- {
- dict_id = dictionaryName,
- });
- var defaultDict = Translations.Default;
- var loading = _defaultTranslationProviders.Select(d => d.GetDefaultTranslations()).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);
- }
-
- public async Task Save(Dictionary dictionary, Translations translations)
- {
- var loadedTranslations = await GetTranslations(dictionary.DictionaryName);
- translations = translations.WithFallback(loadedTranslations.Fallback);
- await using var ctx = _ContextFactory.CreateContext();
- var diffs = loadedTranslations.Translations.CalculateDiff(translations);
- var conn = ctx.Database.GetDbConnection();
- List<string> keys = new List<string>();
- List<string> deletedKeys = new List<string>();
- List<string> values = new List<string>();
-
- // The basic idea here is that we can remove from
- // the dictionary any translations which are the same
- // as the fallback. This way, if the fallback get updated,
- // it will also update the dictionary.
- foreach (var diff in diffs)
- {
- if (diff is Translations.Diff.Added a)
- {
- if (a.Value != loadedTranslations.Fallback[a.Key])
- {
- keys.Add(a.Key);
- values.Add(a.Value);
- }
- }
- else if (diff is Translations.Diff.Modified m)
- {
- if (m.NewValue != loadedTranslations.Fallback[m.Key])
- {
- keys.Add(m.Key);
- values.Add(m.NewValue);
- }
- else
- {
- deletedKeys.Add(m.Key);
- }
- }
- else if (diff is Translations.Diff.Deleted d)
- {
- deletedKeys.Add(d.Key);
- }
- }
- await conn.ExecuteAsync("INSERT INTO lang_translations SELECT @dict_id, sentence, translation FROM unnest(@keys, @values) AS t(sentence, translation) ON CONFLICT (dict_id, sentence) DO UPDATE SET translation = EXCLUDED.translation; ",
- new
- {
- dict_id = loadedTranslations.LangName,
- keys = keys.ToArray(),
- values = values.ToArray()
- });
- await conn.ExecuteAsync("DELETE FROM lang_translations WHERE dict_id=@dict_id AND sentence=ANY(@keys)",
- new
- {
- dict_id = loadedTranslations.LangName,
- keys = deletedKeys.ToArray()
- });
-
- if (_LoadedTranslations.LangName == loadedTranslations.LangName)
- _LoadedTranslations = loadedTranslations with { Translations = translations };
- }
-
- public record Dictionary(string DictionaryName, string? Fallback, string Source, JObject Metadata);
- public async Task<Dictionary[]> GetDictionaries()
- {
- await using var ctx = _ContextFactory.CreateContext();
- var db = ctx.Database.GetDbConnection();
- var rows = await db.QueryAsync<(string dict_id, string? fallback, string? source, string? metadata)>("SELECT * FROM lang_dictionaries");
- return rows.Select(r => new Dictionary(r.dict_id, r.fallback, r.source ?? "", JObject.Parse(r.metadata ?? "{}"))).ToArray();
- }
- public async Task<Dictionary?> GetDictionary(string name)
- {
- await using var ctx = _ContextFactory.CreateContext();
- var db = ctx.Database.GetDbConnection();
- var r = await db.QueryFirstOrDefaultAsync("SELECT * FROM lang_dictionaries WHERE dict_id=@dict_id", new { dict_id = name });
- if (r is null)
- return null;
- return new Dictionary(r.dict_id, r.fallback, r.source ?? "", JObject.Parse(r.metadata ?? "{}"));
- }
-
- public async Task<Dictionary> CreateDictionary(string langName, string? fallback, string source)
- {
- await using var ctx = _ContextFactory.CreateContext();
- var db = ctx.Database.GetDbConnection();
- await db.ExecuteAsync("INSERT INTO lang_dictionaries (dict_id, fallback, source) VALUES (@langName, @fallback, @source)", new { langName, fallback, source });
- return new Dictionary(langName, fallback, source ?? "", new JObject());
- }
-
- public async Task DeleteDictionary(string dictionary)
- {
- await using var ctx = _ContextFactory.CreateContext();
- var db = ctx.Database.GetDbConnection();
- await db.ExecuteAsync("DELETE FROM lang_dictionaries WHERE dict_id=@dict_id AND source='Custom'", new { dict_id = dictionary });
- }
-
- public async Task UpdateDictionaryMetadata(string dictionary, JObject metadata)
- {
- await using var ctx = _ContextFactory.CreateContext();
- var db = ctx.Database.GetDbConnection();
- await db.ExecuteAsync("UPDATE lang_dictionaries SET metadata = @metadata::jsonb WHERE dict_id = @dict_id",
- new { dict_id = dictionary, metadata = metadata.ToString() });
- }
-
- public async Task UpdateVersion(string dictionary, string version)
- {
- await using var ctx = _ContextFactory.CreateContext();
- var db = ctx.Database.GetDbConnection();
- await db.ExecuteAsync("UPDATE lang_dictionaries SET metadata = jsonb_set(COALESCE(metadata, '{}'::jsonb), '{version}', to_jsonb(@version::text)) WHERE dict_id = @dict_id",
- new { dict_id = dictionary, version });
- }
- }
-}
diff --git a/BTCPayServer/Services/Translations.Default.cs b/BTCPayServer/Services/Translations.Default.cs
deleted file mode 100644
index 61aa9e3..0000000
--- a/BTCPayServer/Services/Translations.Default.cs
+++ /dev/null
@@ -1,2115 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace BTCPayServer.Services
-{
- public partial class Translations
- {
- static Translations()
- {
- // Text generated by UpdateDefaultTranslations.
- // Please run it before release.
- var knownTranslations =
-"""
-{
- "... on every payment": "",
- "... only if the customer makes more than one payment for the invoice": "",
- "'{0}' already exists": "",
- "'Anyone can invoice' is turned off": "",
- "({0} migrated users)": "",
- "{0} Archived Store": "",
- "{0} Archived Stores": "",
- "{0} day": "",
- "{0} days": "",
- "{0} files were added. {1} files had invalid names": "",
- "{0} for {1} or {2}": "",
- "{0} invoice archived.": "",
- "{0} invoice unarchived.": "",
- "{0} invoices archived.": "",
- "{0} invoices unarchived.": "",
- "{0} is not fully synched": "",
- "{0} left": "",
- "{0} Lightning": "",
- "{0} Lightning Node": "",
- "{0} Lightning node updated.": "",
- "{0} Lightning Settings": "",
- "{0} Lightning settings successfully updated.": "",
- "{0} minutes": "",
- "{0} Node": "",
- "{0} provider is not supported": "",
- "{0} selected": "",
- "{0} Status": "",
- "{0} Store": "",
- "{0} Stores": "",
- "{0} total": "",
- "{0} Transactions": "",
- "{0} users migrated to the plan '{1}'.": "",
- "{0} wallet": "",
- "{0} Wallet": "",
- "{0} Wallet Labels": "",
- "{0} Wallet Settings": "",
- "@submitLabel": "",
- "<code>itemcode:code</code> for filtering a specific type of item purchased through the pos or crowdfund apps": "",
- "<code>orderid:id</code> for filtering a specific order": "",
- "<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": "",
- "<strong>Never</strong> trust anything but <code>id</code>, <strong>ignore</strong> the other fields completely, an attacker can spoof those, they are present only for backward compatibility reason:": "",
- "1 day": "",
- "24 Hours": "",
- "2FA and U2F/FIDO2 and LNURL-Auth Authentication Methods are not available. Please go to the https endpoint.": "",
- "3 Days": "",
- "30 days": "",
- "7 days": "",
- "7 Days": "",
- "A \"Contact Us\" button with this link will be shown on the checkout page. Can contain the placeholders <code>{OrderId}</code> and <code>{InvoiceId}</code>. Can be any valid URI, such as a website, email, and Nostr.": "",
- "A <code>POST</code> callback will be sent to the specified <code>notificationUrl</code> (for on-chain transactions when there are sufficient confirmations):": "",
- "A camera was not detected on your device.": "",
- "A delay means your service either failed to process earlier deliveries or is taking too long to respond.": "",
- "A given currency pair match the most specific rule. If two rules are matching and are as specific, the first\n rule\n will be chosen.": "",
- "A malicious actor with access to this QR Code can affect the performances of your server.": "",
- "A malicious actor with access to this QR Code could affect the performances of your server and might steal your funds.": "",
- "A malicious actor with access to this QR Code could steal the funds on your lightning wallet.": "",
- "A new payout is approved and awaiting payment": "",
- "A new payout is awaiting for approval": "",
- "A payment request with reference ID \"{0}\" already exists for this store.": "",
- "A payment that was made to an approved payout by an external wallet is waiting for your confirmation.": "",
- "A permission to the camera is needed to scan the QR code. Please grant the browser access and then retry.": "",
- "A Postgres compatible JSON Path (eg. $ ? (@.Customer.Name == \"John\"))": "",
- "A self-hosted, open-source bitcoin payment processor.": "",
- "Access to vault granted by owner.": "",
- "Access Tokens": "",
- "Access Type": "",
- "Account": "",
- "Account created.": "",
- "Account key": "",
- "Account Key": "",
- "Account key path": "",
- "Account successfully created.": "",
- "Account successfully deleted.": "",
- "Action canceled by user": "",
- "Actions": "",
- "Activate Monetization": "",
- "Active": "",
- "Active Members": "",
- "Active Subscribers": "",
- "Add": "",
- "Add additional fee (network fee) to invoice …": "",
- "Add Address": "",
- "Add an email address or an external URL where users can contact you for support requests through a \"Contact Us\" button, displayed at the bottom of the public facing pages.": "",
- "Add destination": "",
- "Add domain mapping": "",
- "Add Exchange Rate Spread": "",
- "Add Form Field": "",
- "Add hop hints for private channels to the Lightning invoice": "",
- "Add Item": "",
- "Add mapped value": "",
- "Add Option": "",
- "Add or customize translations": "",
- "Add plan": "",
- "Add Plan": "",
- "Add plugin manually": "",
- "Add Role": "",
- "Add Service": "",
- "Add subscriber": "",
- "Add User": "",
- "Add Webhook": "",
- "Additional Actions": "",
- "Additional Options": "",
- "Additional rates to track": "",
- "Additional text to provide an explanation for the field": "",
- "Address": "",
- "Address type": "",
- "Address verified.": "",
- "Addresses": "",
- "Adjust the design of your BTCPay Server instance to your needs.": "",
- "Adjusts the generated invoice amount — use as a prefix to have multiple adjustment fields": "",
- "Adjusts the generated invoice amount by multiplying with this value — use as a prefix to have multiple adjustment fields": "",
- "Admin API access token": "",
- "Admin must approve new users": "",
- "Administrator": "",
- "Advanced Options": "",
- "Advanced rate rule scripting": "",
- "Advanced settings": "",
- "All": "",
- "All invoice updates": "",
- "All Labels": "",
- "All notifications are disabled.": "",
- "All Plugins": "",
- "All Status": "",
- "All Stores": "",
- "All Time": "",
- "All Type": "",
- "Allow anyone to create invoice": "",
- "Allow form for public use": "",
- "Allow payee to create invoices with custom amounts": "",
- "Allow payee to pass a comment": "",
- "Allow Stores use the Server's SMTP email settings as their default": "",
- "Already claimed": "",
- "Alternatively, you can use the invoice API by including the following HTTP Header in your requests:": "",
- "Alternatives": "",
- "Always include non-witness UTXO if available": "",
- "Amazon S3": "",
- "Amazon S3 Storage": "",
- "Amount": "",
- "Amount and currency are not editable once payment request has invoices": "",
- "Amount must be greater than 0": "",
- "Amount requested": "",
- "An error occurred while resetting user password": "",
- "An error occurred while saving: {0}": "",
- "An invitation email has been sent.<br/>You may alternatively share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "",
- "An invitation email has not been sent, because the server does not have an email server configured.<br/> You need to share this link with them: <a class='alert-link' href='{0}'>{0}</a>": "",
- "An invoice must be paid within a defined time interval at a fixed exchange rate to protect the issuer from price fluctuations.": "",
- "An unexpected error happened: {0}": "",
- "Animation": "",
- "Any amount": "",
- "Any application using the API key will immediately lose access.": "",
- "Any uploaded files are being saved on the same machine that hosts BTCPay; please pay attention to your storage space.": "",
- "API": "",
- "API authentication docs": "",
- "API Key": "",
- "API key generated!": "",
- "API Key removed": "",
- "API Keys": "",
- "App": "",
- "App deleted successfully.": "",
- "App Item/Perk": "",
- "App Name": "",
- "App not found": "",
- "App successfully created": "",
- "App Type": "",
- "App updated": "",
- "App-based 2FA": "",
- "Application": "",
- "Apply the brand color to the store's backend as well": "",
- "Approve": "",
- "Approve & Send": "",
- "Approve this pairing demand": "",
- "Approve user": "",
- "Approved": "",
- "Archive": "",
- "Archive pull payment": "",
- "Archive this app": "",
- "Archive this app so that it does not appear in the apps list by default": "",
- "Archive this invoice so that it does not appear in the invoice list by default": "",
- "Archive this payment request so that it does not appear in the payment request list by default": "",
- "Archive this store": "",
- "Archive this store so that it does not appear in the stores list by default": "",
- "archived": "",
- "Archived Stores": "",
- "Authenticator code": "",
- "Authorize a public key to access Bitpay compatible Invoice API.": "",
- "Authorize app": "",
- "Authorized keys": "",
- "authorized_keys has been updated": "",
- "Auto-detect language on checkout": "",
- "Automated Bitcoin Sender": "",
- "Automated Lightning Sender": "",
- "Automatic redelivery": "",
- "Automatically approve claims": "",
- "Automatically Approved": "",
- "available": "",
- "Available claim": "",
- "Available Filters (click to expand)": "",
- "Available Payment Methods": "",
- "Available placeholders: <code>{StoreName} {ItemDescription} {OrderId}</code>": "",
- "Available Plugins": "",
- "Awaiting": "",
- "Azure Blob Storage": "",
- "Backend's language": "",
- "Balance": "",
- "Base URL": "",
- "Batch size": "",
- "BCC": "",
- "Before being able to upload you first need to {0}.": "",
- "Before you proceed, please understand the following:": "",
- "BIP39 Seed (12/24 word mnemonic phrase) or HD private key (xprv...)": "",
- "Block Explorers": "",
- "blocks": "",
- "Boltcard is configured": "",
- "Boltcard URL": "",
- "Brand Color": "",
- "Branding": "",
- "Broadcast (Payjoin)": "",
- "Broadcast (Simple)": "",
- "Broadcast transaction": "",
- "Browser connection": "",
- "Browser Redirect": "",
- "BTCPay exposes Core Lightning's REST service for outside consumption, you will find connection information here.": "",
- "BTCPay is expecting you to access this website from <strong>": "",
- "BTCPay Server Configurator": "",
- "BTCPay Server currently supports:": "",
- "BTCPay Server Registration will redirect to a custom registration page": "",
- "BTCPay Server Supporters": "",
- "BTCPay will restart momentarily.": "",
- "Bump fee": "",
- "But now, what if you want to support <code>DOGE</code>? The problem with <code>DOGE</code> is that most\n exchange do\n not have any pair for it. But <code>bitpay</code> has a <code>DOGE_BTC</code> pair.<br />Luckily, the rule engine allow you to\n reference\n rules:": "",
- "Button Type": "",
- "Buy Button Text": "",
- "Buyer Email": "",
- "By confirming, you will deactivate the monetization feature, user access will not be dependent on subscriptions anymore.": "",
- "By proceeding, all non-admin users will be migrated to the selected plan. If the plan does not include the <b>can-access</b> feature, the user accounts will be disabled.": "",
- "Callback Notification URL": "",
- "Campaign not active": "",
- "Can access BTCPay Server": "",
- "Can create a new cold wallet": "",
- "Can use hot wallet": "",
- "Can use RPC import": "",
- "Cancel": "",
- "Cancel Invoice": "",
- "Cannot delete plan. It is currently in use by subscribers.": "",
- "Cannot generate API keys while not using HTTPS or Tor": "",
- "Card reset succeed": "",
- "Categories": "",
- "Caution: Allowing non-admins to have access to API endpoints may expose your BTCPay Server instance to potential security risks from unknown users.": "",
- "Caution: Enabling public user registration means anyone can register to your server and may expose your BTCPay Server instance to potential security risks from unknown users.": "",
- "Caution: Enabling this option, may simplify the onboarding and spending for third-parties but carries liabilities and security risks associated to storing private keys of third parties on a server.": "",
- "Caution: Enabling this option, may simplify the onboarding for third-parties but carries liabilities and security risks associated with sharing the lightning node with other users.": "",
- "CC": "",
- "Celebrate payment with confetti": "",
- "Change": "",
- "Change connection": "",
- "Change domain": "",
- "Change Role": "",
- "Change Storage provider": "",
- "Change your {0} provider.": "",
- "Change your password": "",
- "Changes to the SSH settings are now permanently disabled in the BTCPay Server user interface": "",
- "Changing the role of user {0} failed: {1}": "",
- "Charge": "",
- "Charge user": "",
- "Cheat Mode: Send funds to this wallet": "",
- "Check if NFC is supported and enabled on this device": "",
- "Check releases on GitHub and notify when new BTCPay Server version is available": "",
- "Checking BTCPay Server Vault is running...": "",
- "Checking if this device can sign the transaction...": "",
- "Checkout": "",
- "Checkout Additional Query String": "",
- "Checkout Appearance": "",
- "Checkout Description": "",
- "Checkout Experience": "",
- "Choose a different offering for monetization": "",
- "Choose a language...": "",
- "Choose Point of Sale Style": "",
- "Choose what event sends the email.": "",
- "choose your file storage service provider": "",
- "Choose your import method": "",
- "Choose your signing method": "",
- "Choose your wallet option": "",
- "Choosing to accept an unconfirmed invoice can lead to double-spending and is strongly discouraged.": "",
- "Claim Funds": "",
- "Claim limit": "",
- "Claimed": "",
- "Claims": "",
- "Clean": "",
- "Clear": "",
- "Clear All": "",
- "Clear all filters": "",
- "Clear all transactions from history": "",
- "Clear filter": "",
- "Clear label filter": "",
- "click here": "",
- "Clone": "",
- "Close": "",
- "Code": "",
- "Coin selection": "",
- "Collect signatures": "",
- "Colors to rotate between with animation when a payment is made. One color per line.": "",
- "Combine": "",
- "Combine filters:": "",
- "Combine PSBT": "",
- "Comma-separated list of currencies (eg. USD,EUR,JPY)": "",
- "Compatible wallets": "",
- "Completed": "",
- "CONFIDENTIAL: This QR Code is confidential, close this window as soon as you don't need it anymore.": "",
- "Config file was not in the correct format": "",
- "Configure": "",
- "Configure app": "",
- "Configure email": "",
- "Configure now": "",
- "Configure offering": "",
- "Configure store email settings": "",
- "Configure your Pay Button, and the generated code will be displayed at the bottom of the page to copy into your project.": "",
- "Configured": "",
- "Configuring Boltcard...": "",
- "Confirm": "",
- "Confirm addresses": "",
- "Confirm broadcasting this transaction": "",
- "Confirm in the next …": "",
- "Confirm Lightning Payout": "",
- "Confirm new password": "",
- "Confirm passphrase": "",
- "Confirm password": "",
- "Confirmations": "",
- "confirmed": "",
- "Connect an existing wallet": "",
- "Connect BTCPay Server to your Shopify checkout experience to accept Bitcoin.": "",
- "Connect hardware wallet": "",
- "Connect to a Lightning node": "",
- "Connect your hardware wallet": "",
- "Connection configuration for your custom Lightning node:": "",
- "Connection string": "",
- "Connection String": "",
- "Connection to the Lightning node successful.": "",
- "Consider the invoice paid even if the paid amount is … % less than expected": "",
- "Consider the invoice settled when the payment transaction …": "",
- "Constant": "",
- "Constraints do not match any installed camera.": "",
- "Contact URL": "",
- "Contact Us": "",
- "Container Name": "",
- "Continue": "",
- "Contribute": "",
- "contribution": "",
- "Contribution Amount": "",
- "Contribution Perks Template": "",
- "contributions": "",
- "Contributions": "",
- "Contributions allowed even after goal is reached": "",
- "Contributors": "",
- "Copy Code": "",
- "Copy Link": "",
- "Copy Tor URL": "",
- "Core Lightning {0}": "",
- "Could not access your camera. Is it already in use?": "",
- "Could not generate invoice: {0}": "",
- "Could not load log files": "",
- "Could not save CSS file: {0}": "",
- "Could not save image: {0}": "",
- "Could not save logo: {0}": "",
- "Could not save sound: {0}": "",
- "Count all invoices created on the store as part of the goal": "",
- "Create": "",
- "Create {0} Hot Wallet": "",
- "Create {0} Watch-Only Wallet": "",
- "Create a new {0}": "",
- "Create a new app": "",
- "Create a new offering": "",
- "Create a new store": "",
- "Create a new subscriber": "",
- "Create a new wallet": "",
- "create a separate store": "",
- "Create a store": "",
- "Create a store to begin accepting payments.": "",
- "Create account": "",
- "Create Account": "",
- "Create Email Rule": "",
- "Create Form": "",
- "Create Invoice": "",
- "Create invoice to pay custom amount": "",
- "Create Invoices": "",
- "Create New Token": "",
- "Create Payment Request": "",
- "Create pending transaction": "",
- "Create Pull Payment": "",
- "Create refund": "",
- "Create Request": "",
- "Create role": "",
- "Create Store": "",
- "Create template from selected store": "",
- "Create temporary file link": "",
- "Create Token": "",
- "Create Webhook": "",
- "Create your account": "",
- "Create your first store": "",
- "Create your store": "",
- "Created": "",
- "Created after date:": "",
- "Created at": "",
- "Created before date:": "",
- "Credentials": "",
- "Credit": "",
- "Credit user": "",
- "Credits": "",
- "Crowdfund": "",
- "Crowdfund Behavior": "",
- "Crypto": "",
- "Crypto Code": "",
- "Crypto services exposed by your server": "",
- "CSV": "",
- "Currency": "",
- "Currency is invalid": "",
- "Currency Pair Testing": "",
- "Currency pairs to test against your rule": "",
- "Current effective fee rate": "",
- "Current password": "",
- "Current Rates source is": "",
- "Currently active!": "",
- "Custom": "",
- "Custom amount": "",
- "Custom Amount": "",
- "Custom checkout text": "",
- "Custom CSS": "",
- "Custom data to expand the invoice. This data is a JSON object, e.g. <code>{ \"orderId\": 615, \"product\": \"Pizza\" }</code>": "",
- "Custom HTML title to display on Checkout page": "",
- "Custom Payments": "",
- "Custom Range": "",
- "Custom sound file for successful payment": "",
- "Custom text displayed on the checkout page below the payment details. Plain text only, newlines are supported.": "",
- "Custom Theme Extension Type": "",
- "Custom Theme File": "",
- "Customer Email": "",
- "Customer Information": "",
- "Customization": "",
- "Customize Pay Button Text": "",
- "Dark": "",
- "Dashboard": "",
- "Date": "",
- "days": "",
- "Decode PSBT": "",
- "Default": "",
- "Default currency": "",
- "Default Currency Pairs": "",
- "Default dictionary changed to {0}": "",
- "Default Include NonWitness Utxo in PSBTs": "",
- "Default language on checkout": "",
- "Default Payment Method": "",
- "Default payment method on checkout": "",
- "Default role for users on a new store": "",
- "Default store template": "",
- "Default Tax Rate": "",
- "Default Value": "",
- "Delay": "",
- "Delete": "",
- "DELETE": "",
- "Delete Account": "",
- "Delete admin": "",
- "Delete API key": "",
- "Delete app": "",
- "Delete dictionary": "",
- "Delete label": "",
- "Delete LND seed": "",
- "Delete LND seed from server": "",
- "Delete offering": "",
- "Delete role": "",
- "Delete store": "",
- "Delete store {0}": "",
- "Delete this app": "",
- "Delete this store": "",
- "Delete unused Docker images present on your system.": "",
- "Delete user": "",
- "Delete Webhook": "",
- "Delivered at": "",
- "Demonetize": "",
- "Dependencies": "",
- "Dependencies not met.": "",
- "Derivation scheme": "",
- "Description": "",
- "Description template of the lightning invoice": "",
- "Destination": "",
- "Destination Address": "",
- "Details": "",
- "Detects the language of the customer's browser.": "",
- "Determine the generated invoice amount": "",
- "Determine the generated invoice currency": "",
- "Device found: {0}": "",
- "Dictionaries": "",
- "Dictionaries enable you to translate the BTCPay Server backend into different languages.": "",
- "Dictionary": "",
- "Dictionary {0} deleted": "",
- "Dictionary created": "",
- "Dictionary not found or not a downloadable language pack": "",
- "Dictionary updated": "",
- "Direct integration": "",
- "Disable": "",
- "DISABLE": "",
- "Disable 2FA": "",
- "Disable admin": "",
- "Disable all notifications": "",
- "Disable modification of SSH settings": "",
- "Disable payment button": "",
- "Disable public user registration": "",
- "Disable stores from using the server's email settings as backup": "",
- "Disable two-factor authentication (2FA)": "",
- "Disable zero amount invoices": "",
- "Disabled": "",
- "Disabled Plugins": "",
- "Disabling 2FA does not change the keys used in the authenticator apps. If you wish to change the key used in an authenticator app you should reset your authenticator keys.": "",
- "Disabling will delete your rate script.": "",
- "Discount": "",
- "Discounts": "",
- "Discourage search engines from indexing this site": "",
- "Discussion": "",
- "Display app on website root": "",
- "Display contribution ranking": "",
- "Display contribution value": "",
- "Display item selection for keypad": "",
- "Display Lightning payment amounts in Satoshis": "",
- "Display Options": "",
- "Display the category list": "",
- "Display the key or QR code to configure an authenticator app with your current setup.": "",
- "Display the search bar": "",
- "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?": "",
- "Docs": "",
- "Documentation": "",
- "Does not extend a BTCPay Server theme, fully custom": "",
- "Domain": "",
- "Domain name": "",
- "Domain name changing... the server will restart, please use \"{0}\" (this page won't reload automatically)": "",
- "Domain to app mapping": "",
- "Don't create UTXO change": "",
- "Donate": "",
- "Done": "",
- "Downgrade": "",
- "Download": "",
- "Download a two-factor authenticator app like …": "",
- "Download language pack": "",
- "Download Language Pack": "",
- "Download PSBT file": "",
- "Dynamic": "",
- "Dynamic DNS allows you to have a stable DNS name pointing to your server, even if your IP address changes regularly. This is recommended if you are hosting BTCPay Server at home and wish to have a clearnet domain to access your server.": "",
- "Dynamic DNS Service": "",
- "Dynamic DNS service successfully removed": "",
- "Dynamic DNS Settings": "",
- "Each address generated will be imported into the node wallet and you can view your balance through the node.": "",
- "Each payment method shows the total excess amount.": "",
- "Easily filter the different items using categories, used only in the product list with cart.": "",
- "Easily log into BTCPay Server on another device using a simple login code from an already authenticated device.": "",
- "Edit": "",
- "Edit Field": "",
- "Edit Form": "",
- "Edit Item": "",
- "Edit Label": "",
- "Edit payment request": "",
- "Edit Payment Request": "",
- "Edit plan": "",
- "Edit plan ({0})": "",
- "Edit pull payment": "",
- "Edit Pull Payment": "",
- "Editor": "",
- "Either your {0} wallet is not configured, or it is not a hot wallet. This processor cannot function until a hot wallet is configured in your store.": "",
- "Email": "",
- "Email address": "",
- "Email address is confirmed": "",
- "Email Configuration": "",
- "Email confirmation required": "",
- "Email Confirmed": "",
- "Email confirmed?": "",
- "Email Notifications": "",
- "Email password reset functionality is not configured for this server. Please contact the server administrator to assist with account recovery.": "",
- "Email Reminder Days Before Due": "",
- "Email rule successfully created": "",
- "Email rule successfully deleted": "",
- "Email rule successfully updated": "",
- "Email rules": "",
- "Email Rules": "",
- "Email rules allow BTCPay Server to send customized emails from your server based on events.": "",
- "Email rules allow BTCPay Server to send customized emails from your store based on events.": "",
- "Email sent to {0}. Please verify you received it.": "",
- "Email server password reset": "",
- "Email settings saved": "",
- "Emails": "",
- "Embed a payment button linking to POS item": "",
- "Embed Point of Sale via iframe": "",
- "Empty": "",
- "Enable": "",
- "Enable 2FA": "",
- "Enable advanced rate rule scripting": "",
- "Enable Authenticator App": "",
- "Enable background animations on new payments": "",
- "Enable Disqus Comments": "",
- "Enable experimental features": "",
- "Enable fallback rates": "",
- "Enable LNURL": "",
- "Enable notifications": "",
- "Enable PayJoin": "",
- "Enable Payjoin/P2EP": "",
- "Enable payment methods only when amount is …": "",
- "Enable public receipt page for settled invoices": "",
- "Enable public user registration": "",
- "Enable sounds on checkout page": "",
- "Enable sounds on new payments": "",
- "Enable tips": "",
- "Enable trial": "",
- "Enabled": "",
- "Enabling will modify your current rate sources. This is a feature for advanced users.": "",
- "End date": "",
- "End Date": "",
- "Ends {0}": "",
- "Ends in": "",
- "Enhance the checkout process for in-store purchases.<br />This assumes the payment page will be displayed on the merchant's device.": "",
- "Enhance the checkout process for online purchases.<br />This assumes the payment page will be displayed on the customer's device.": "",
- "Enter destination to claim funds": "",
- "Enter extended public key": "",
- "Enter the code in the confirmation box below.": "",
- "Enter the passphrase.": "",
- "Enter the pin.": "",
- "Enter the wallet seed": "",
- "Enter wallet seed": "",
- "Enter your extended public key": "",
- "Error": "",
- "Error updating profile": "",
- "Error updating user": "",
- "Error while broadcasting: {0}": "",
- "Example": "",
- "Expiration Date": "",
- "Expire": "",
- "Expire invoice in …": "",
- "Expired": "",
- "expired with partial payments": "",
- "Expires in": "",
- "Export": "",
- "Export the PSBT for your wallet. Sign it with your wallet and import the signed PSBT version here for finalization and broadcasting.": "",
- "Extended public key": "",
- "Extends the BTCPay Server Dark theme": "",
- "Extends the BTCPay Server Light theme": "",
- "External payout approval": "",
- "Failed to archive the app.": "",
- "Failed to download language pack: {0}": "",
- "Failed to unarchive the app.": "",
- "Failed to update language pack: {0}": "",
- "Fallback": "",
- "Fallback rate source": "",
- "Fallback rates will be used in case the primary rates are not available.": "",
- "Feature disabled": "",
- "Featured Image URL": "",
- "Features": "",
- "Fee block target": "",
- "Fee bump method": "",
- "Fee rate": "",
- "Fee rate (sat/vB)": "",
- "Fee will be shown for BTC and LTC onchain payments only.": "",
- "Fetching device...": "",
- "Fetching public keys...": "",
- "Fetching wallet's fingerprint.": "",
- "FIDO2 Authentication": "",
- "Field to mirror": "",
- "File Id": "",
- "File Logging Option not specified. You need to set debuglog and optionally debugloglevel in the configuration or through runtime arguments": "",
- "File name": "",
- "File removed": "",
- "file storage": "",
- "File Storage": "",
- "file storage service": "",
- "File with wallet password and seed info not present": "",
- "Files": "",
- "Files added successfully": "",
- "Files could not be added due to invalid names": "",
- "Files uploaded, restart server to load plugins": "",
- "Fill fake": "",
- "Fill out currency pair to test for (like {0})": "",
- "Filter": "",
- "Filter by address, label or date": "",
- "Filter by label": "",
- "Filter by transaction id, amount, label, comment": "",
- "Filter invoices by Custom Range": "",
- "Filter payment requests by Custom Range": "",
- "Firstname Lastname <email@example.com>": "",
- "Fit button inline": "",
- "Fixed amount": "",
- "follow these instructions": "",
- "For a specific item of your template": "",
- "For anything with a custom amount": "",
- "for lifetime": "",
- "For many email providers (like Gmail) your login is your email address.": "",
- "For the macaroon options you need to provide a macaroon with the <code>invoices:write</code> permission (e.g. <code>invoice.macaroon</code>. If you want to display the node connection details, it also needs the <code>info:read</code> permission.": "",
- "For wallet compatibility: Bech32 encoded (classic) vs. cleartext URL (upcoming)": "",
- "Forgot password?": "",
- "Form config was invalid: {0}": "",
- "Form configuration (JSON)": "",
- "Form created successfully.": "",
- "Form JSON": "",
- "Form removed": "",
- "Form updated successfully.": "",
- "Forms": "",
- "Free": "",
- "Full node connection": "",
- "Gap limit": "",
- "General": "",
- "General Settings": "",
- "General text search:": "",
- "Generate": "",
- "Generate {0} Wallet": "",
- "Generate a brand-new wallet to use": "",
- "Generate a new api key to use BTCPay through its API.": "",
- "Generate a QR code of the extended public key in your wallet (see instructions for supported wallets below).\n Allow the browser access to your camera and hold the code to the camera when the scan prompt appears.": "",
- "Generate another address": "",
- "Generate API Key": "",
- "Generate Key": "",
- "Generated Code": "",
- "Get Link": "",
- "Give other registered BTCPay Server users access to your store. See the {0} for granted permissions.": "",
- "Go back to Javascript enabled invoice": "",
- "Go to email rules": "",
- "Go to top": "",
- "Google Cloud Storage": "",
- "Grace": "",
- "Grace Period": "",
- "Grace Period (days)": "",
- "Greater than": "",
- "Greenfield API": "",
- "Greenfield API Keys": "",
- "GRPC SSL Cipher suite (GRPC_SSL_CIPHER_SUITES)": "",
- "Hardcap Goal": "",
- "Hardware wallet": "",
- "Has at least 1 confirmation": "",
- "Has at least 2 confirmations": "",
- "Has at least 6 confirmations": "",
- "has payments that failed to confirm on time": "",
- "Has Store": "",
- "Helper Text": "",
- "here": "",
- "Hide coin selection": "",
- "Hide Sensitive Info": "",
- "Hide unconfirmed coins": "",
- "Hostname": "",
- "Hot wallet": "",
- "How much to refund?": "",
- "However, <code>kraken</code> does not support the <code>BTC_CAD</code> pair. For this reason you can add a rule\n mapping all <code>X_CAD</code> to <code>ndax</code>, a Canadian exchange.": "",
- "However, explicitely setting specific pairs like this can be a bit difficult. Instead, you can define a rule\n <code>X_X</code>\n which will match any currency pair. The following example will use <code>kraken</code> for getting the rate of any currency pair.": "",
- "HTML Headers": "",
- "HTML Lang": "",
- "HTML Meta Tags": "",
- "HTTP-based Tor hidden services": "",
- "I have written down my recovery phrase and stored it in a secure location": "",
- "I wrote down my recovery codes": "",
- "Id": "",
- "ID": "",
- "If a translation isn’t available in the new dictionary, it will be searched in the fallback.": "",
- "If authorized, the generated API key will be provided to": "",
- "If BTCPay Server shows you an invalid balance, {0}.<br />If some transactions appear in BTCPay Server, but are missing in another wallet, {1}.": "",
- "If checked, each private key associated with an address generated will be stored as metadata and would be accessible to anyone with admin access to your server. Enable at your own risk!": "",
- "If you change your configured storage provider, your current files will become inaccessible.": "",
- "If you do not understand the above, go with the defaults and start scanning.": "",
- "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.": "",
- "If you lose your device and don't have the recovery codes you will lose access to your account.": "",
- "If your LND REST server is using HTTP or HTTPS with an untrusted certificate, you can set\n <code>allowinsecure=true</code> as a fallback.": "",
- "If your security device has a button, tap on it.": "",
- "Image": "",
- "Image Size": "",
- "Image uploaded successfully": "",
- "Image Url": "",
- "Import {0} Wallet": "",
- "Import an existing hardware or software wallet": "",
- "Import failed, make sure you import a compatible wallet format": "",
- "Import keys to RPC": "",
- "Import wallet file": "",
- "Import your public keys using our Vault application": "",
- "Import your wallet file": "",
- "Important notice": "",
- "Important notice about plugins": "",
- "Impossible to fetch rate: {0}": "",
- "In order to securely connect to your hardware wallet you must first download, install, and run the BTCPay Server Vault.": "",
- "In order to upload, a {0} must be configured.": "",
- "In Shopify, please do the following …": "",
- "In use": "",
- "In-store": "",
- "Inactive": "",
- "Include archived": "",
- "Include Archived": "",
- "Incorrect pin code.": "",
- "Increase the security of your instance by disabling the ability to change the SSH settings in this BTCPay Server instance's user interface.": "",
- "Index": "",
- "Input the key string manually": "",
- "Inputs": "",
- "Insert your security device and proceed.": "",
- "Install": "",
- "Installed Plugins": "",
- "Instructions": "",
- "Interval": "",
- "Invalid": "",
- "Invalid App Type": "",
- "Invalid Base URL": "",
- "Invalid currency": "",
- "Invalid currency pair '{0}' (it should be formatted like {1})": "",
- "Invalid currency pairs (should be for example: {0})": "",
- "Invalid destination or payment method": "",
- "Invalid email": "",
- "Invalid email address or placeholder detected": "",
- "Invalid file name": "",
- "Invalid login attempt.": "",
- "Invalid network": "",
- "Invalid passphrase confirmation": "",
- "Invalid password confirmation.": "",
- "Invalid payout method": "",
- "Invalid PSBT": "",
- "Invalid role": "",
- "Invalid store": "",
- "Invalid wallet format: {0}": "",
- "Invalidates the current authenticator configuration. Useful if you believe your authenticator settings were compromised.": "",
- "Inventory": "",
- "Inventory for {0} exhausted: {1} available": "",
- "Invitation accepted. Please set your password.": "",
- "Invitation URL": "",
- "Invoice": "",
- "Invoice {0}": "",
- "Invoice {0} just created!": "",
- "Invoice {0}..": "",
- "Invoice currency": "",
- "Invoice expires if the full amount has not been paid after …": "",
- "Invoice Id": "",
- "Invoice is not overpaid": "",
- "Invoice metadata": "",
- "Invoice Notifications": "",
- "Invoices": "",
- "Invoices are documents issued by the seller to a buyer to collect payment.": "",
- "Is Admin": "",
- "Is administrator?": "",
- "Is hot wallet": "",
- "Is MultiSig on Server": "",
- "Is Set Up": "",
- "is settled": "",
- "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 />It means that the\n rule <code>USD_DOGE = 1 / DOGE_USD</code> implicitly exists.": "",
- "Item Description": "",
- "Items total": "",
- "JSON": "",
- "JSON Credentials": "",
- "Keep empty for server-initiated pairing.": "",
- "Key": "",
- "Key path": "",
- "Keypad": "",
- "Label": "",
- "Label Name": "",
- "Label name cannot be empty.": "",
- "Label:": "",
- "Labels": "",
- "Language pack '{0}' downloaded successfully": "",
- "Language pack '{0}' updated successfully": "",
- "Last 24 hours": "",
- "Last 3 days": "",
- "Last 7 days": "",
- "Last delivery {0}": "",
- "Last error:": "",
- "Last updated": "",
- "Last Updated": "",
- "Learn More": "",
- "Leave blank to generate ID from title.": "",
- "Leave blank to not use this feature.": "",
- "Legacy": "",
- "Legacy (Not recommended)": "",
- "Legacy API Keys": "",
- "Less than": "",
- "Let's get started": "",
- "Light": "",
- "Lightning Address": "",
- "Lightning address {0} removed successfully.": "",
- "Lightning address added successfully.": "",
- "Lightning Balance": "",
- "Lightning charge is a simple API for invoicing on lightning network, you can use it with several plugins:": "",
- "Lightning Charge Service": "",
- "Lightning Enabled": "",
- "Lightning network settings": "",
- "Lightning node (LNURL Auth)": "",
- "Lightning Payout Processor": "",
- "Lightning Payout Result": "",
- "Lightning Services": "",
- "Lightning Supported": "",
- "Limit": "",
- "Link": "",
- "Link this Pay Button to an app instead. Some features are disabled due to the different endpoint capabilities. You can set which perk/item this button should be targeting.": "",
- "Link URL": "",
- "LND {0}": "",
- "LND Seed Backup": "",
- "LNURL": "",
- "LNURL Auth was removed successfully.": "",
- "LNURL Authentication": "",
- "LNURL Classic Mode": "",
- "LNURL is required for lightning addresses but has not yet been enabled.": "",
- "LNURL or LN is disabled": "",
- "LNURL-Withdraw": "",
- "Loading...": "",
- "Local File System": "",
- "Local Filesystem Storage": "",
- "Log in": "",
- "Login": "",
- "Login code was invalid": "",
- "Login Codes": "",
- "Logo": "",
- "Logout": "",
- "Logs": "",
- "Mails": "",
- "Maintenance": "",
- "Maintenance feature requires access to SSH properly configured in BTCPay Server configuration.": "",
- "Make Crowdfund Public": "",
- "Make sure this BTCPay Server instance belongs to you.": "",
- "Manage": "",
- "Manage Account": "",
- "Manage billing": "",
- "Manage labels": "",
- "Manage Labels": "",
- "Manage Plugins": "",
- "Manually enter your 12 or 24 word recovery seed.": "",
- "Map specific domains to specific apps": "",
- "Mapped Value": "",
- "Mark as a test account": "",
- "Mark as already paid": "",
- "Mark as awaiting payment": "",
- "Mark as invalid": "",
- "Mark as seen": "",
- "Mark as settled": "",
- "Marked for deletion": "",
- "Marked for enabling": "",
- "Master fingerprint": "",
- "Max": "",
- "Max sats": "",
- "Maximum amount of sats to allow to be sent to this ln address": "",
- "Maximum amount:": "",
- "Memo": "",
- "Message": "",
- "Metadata": "",
- "Metadata must be a valid JSON object": "",
- "Metadata was not valid JSON": "",
- "Method": "",
- "Migrate existing non-admin users": "",
- "Migrate existing users": "",
- "Min": "",
- "Min sats": "",
- "Mine": "",
- "Mine to test processing and settlement": "",
- "Minimum acceptable expiration time for BOLT11 for refunds": "",
- "Minimum amount of sats to allow to be sent to this ln address": "",
- "Minimum amount:": "",
- "minutes": "",
- "Mirror of": "",
- "Modify": "",
- "Monetization": "",
- "Monetization allows you to get paid for sharing your BTCPay Server instance with other users.": "",
- "Monetization deactivated, users who register to your server from now will not be subscriber of any offering.": "",
- "Monetization order updated to offering {0} with default plan {1}.": "",
- "Monthly revenue": "",
- "More details": "",
- "More details...": "",
- "More information": "",
- "More information...": "",
- "Name": "",
- "Navigate back to home": "",
- "NBXplorer headers height: {0}": "",
- "NBXplorer is synchronizing... (Height: {0})": "",
- "NBXplorer is unable to track this derivation scheme. You may need to update it.": "",
- "Negative amount is not allowed": "",
- "Negative tip or discount is not allowed": "",
- "Network Fee": "",
- "Never add network fee": "",
- "New {0} plugin version {1} released!": "",
- "New Card": "",
- "New effective fee rate": "",
- "New offering": "",
- "New offering created. You can now <a href='{0}' class='alert-link'>configure it.</a>": "",
- "New password": "",
- "New plan created": "",
- "New role": "",
- "New user {0} requires approval.": "",
- "New user requires approval": "",
- "New version": "",
- "New Version": "",
- "New version {0} released!": "",
- "Next": "",
- "NFC detected.": "",
- "No": "",
- "No access tokens yet.": "",
- "No claim made yet.": "",
- "No contributions allowed after the goal has been reached": "",
- "No contributions have been made yet.": "",
- "No deliveries for this webhook yet": "",
- "No device connected.": "",
- "No documentation": "",
- "No email address has been configured for the Server. Configure an email address\n to\n begin sending emails.": "",
- "No end date has been set": "",
- "No expiry date has been set for this payment request": "",
- "No invoice has been selected": "",
- "No matching payment method": "",
- "No payment handler found for this payment method": "",
- "No payment request related email rules have been configured for this store.": "",
- "No payments have been made yet.": "",
- "No payout selected": "",
- "No permissions": "",
- "No plugins found": "",
- "No policies": "",
- "No public address available.": "",
- "No public address has been configured.": "",
- "No reserved addresses found.": "",
- "No sales have been made yet.": "",
- "No scope": "",
- "No start date has been set": "",
- "No stores": "",
- "No transaction selected": "",
- "No unpaid pending invoice to cancel": "",
- "No users": "",
- "Node headers height: {0}": "",
- "Node Info": "",
- "Non-admins can access the User Creation API Endpoint": "",
- "Non-admins can create Cold Wallets for their Store": "",
- "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": "",
- "Non-supported state of invoice": "",
- "None of the selected transaction can be fee bumped": "",
- "Normal": "",
- "Not all payout methods are supported": "",
- "Not allowed to cancel this invoice": "",
- "Not available in Keypad POS": "",
- "Not configured": "",
- "Not recommended": "",
- "Not recommended for customer self-checkout.": "",
- "Note that <code>bitcoin-host</code> and <code>bitcoin-auth</code> are optional, only useful if you want to use <code>GetDepositAddress</code>\n on Eclair:": "",
- "Note: {0} are still immature and require additional confirmations.": "",
- "Notification Email": "",
- "Notification Settings": "",
- "Notification URL": "",
- "Notification URL Callbacks": "",
- "Notifications": "",
- "Notifications & Alerts": "",
- "Offering ({0})": "",
- "Offering configuration updated": "",
- "Offline signing, without connecting your wallet to the internet": "",
- "On-Chain Payments": "",
- "On-Chain Payout Processor": "",
- "Online": "",
- "Only enable the payment method after user explicitly chooses it": "",
- "Only meta tags are allowed in HTML headers. Your HTML code has been cleaned up accordingly.": "",
- "Only process payouts when this payout sum is reached.": "",
- "Only send email when the specified JSON Path exists": "",
- "Only upload plugins from trusted sources.": "",
- "Open in wallet": "",
- "Optimistic activation": "",
- "optional": "",
- "Optional passphrase (BIP39)": "",
- "Optional seed passphrase": "",
- "Optional: Specify the percentage by which to reduce the refund, e.g. as processing charge or to compensate for the mining fee.": "",
- "Options": "",
- "or": "",
- "or more": "",
- "Order Id": "",
- "Order ID": "",
- "Original transaction": "",
- "Original Value": "",
- "Other actions": "",
- "Other external services": "",
- "Other Tor hidden services": "",
- "Otherwise you are exposing yourself to malicious site owners, or to malicious plugins installed in your browser.": "",
- "Otherwise, the server's SMTP settings will be used to send emails.": "",
- "Outputs": "",
- "Overpaid": "",
- "Overpaid amount": "",
- "Overpaid amount cannot be calculated": "",
- "Override the block explorers used": "",
- "Page Size": "",
- "Page Size:": "",
- "Paid": "",
- "Paid invoices in the last {0} days": "",
- "Pair to": "",
- "Pair To Store": "",
- "Pairing Permission": "",
- "Parsing error: {0}": "",
- "Partially Signed Bitcoin Transaction": "",
- "Passphrase (Leave empty if there isn't any passphrase)": "",
- "Passphrase confirmation": "",
- "Password": "",
- "Password (leave blank to generate invite-link)": "",
- "Password entered...": "",
- "Password Reset": "",
- "Password successfully set": "",
- "Password successfully set.": "",
- "Paste BIP21": "",
- "Pay": "",
- "Pay Button": "",
- "Pay Button Image Url": "",
- "Pay Button request failed": "",
- "Pay Button Text": "",
- "Pay Invoice": "",
- "Paying via this payment method is not supported": "",
- "PayJoin BIP21": "",
- "PayJoin enhances the privacy for you and your customers. Enabling it gives your customers the option to use PayJoin during checkout.": "",
- "Payjoin transaction": "",
- "Payload URL": "",
- "Payment": "",
- "Payment cancelled": "",
- "Payment Details": "",
- "Payment History": "",
- "Payment invalid if transactions fails to confirm … after invoice expiration": "",
- "Payment Link": "",
- "Payment method": "",
- "Payment Method": "",
- "Payment Notifications": "",
- "Payment Proof": "",
- "Payment received, waiting for confirmation...": "",
- "Payment request \"{0}\" created successfully": "",
- "Payment request \"{0}\" updated successfully": "",
- "Payment Request cannot be paid as it has been archived": "",
- "Payment Request has already been settled.": "",
- "Payment Request has expired": "",
- "Payment Request Labels": "",
- "payment requests": "",
- "Payment Requests": "",
- "Payment requests are persistent shareable pages that enable the receiver to pay at their convenience. Funds are paid to a payment request at the current exchange rate.": "",
- "Payments": "",
- "Payout Methods": "",
- "Payout Processor removed": "",
- "Payout Processors": "",
- "Payout Processors allow BTCPay Server to handle payouts in an automated way.": "",
- "Payout Processors help automate payouts so that you do not need to manually handle them.": "",
- "Payouts": "",
- "Payouts allow you to process pull payments, in the form of refunds, salary payouts, or withdrawals.": "",
- "Payouts approved": "",
- "Payouts archived": "",
- "Payouts for pull payment {0}": "",
- "Payouts marked as paid": "",
- "Payouts Pending": "",
- "Pending": "",
- "Pending Action": "",
- "Pending actions": "",
- "Pending Actions": "",
- "Pending Approval": "",
- "Pending Email Verification": "",
- "Pending Invitation": "",
- "per month": "",
- "per quarter": "",
- "per year": "",
- "percent": "",
- "Percentage must be a numeric value between 0 and 100": "",
- "Permanent Url": "",
- "Permissions": "",
- "Phase": "",
- "Pin code verified.": "",
- "Placeholder": "",
- "Placeholders": "",
- "Plan": "",
- "Plan deleted": "",
- "Plan edited": "",
- "Plan ID": "",
- "Plan Name": "",
- "Plans": "",
- "Please check that your wallet is generating the same addresses as below.": "",
- "Please check your addresses and confirm.": "",
- "Please check your email to reset your password.": "",
- "Please configure it first.": "",
- "Please consult the server log for more details.": "",
- "Please contact support.": "",
- "Please enable JavaScript for this option to be available": "",
- "Please enter a positive amount": "",
- "Please fix errors shown in order for code generation to successfully execute.": "",
- "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.": "",
- "Please note that creating a wallet is not supported by your instance.": "",
- "Please note that not all text is translatable, and future updates may modify existing translations or introduce new translatable phrases.": "",
- "Please note that this instance does not support creating a new cold wallet for non-administrators. However, you can import one from other wallet software.": "",
- "Please provide a connection string": "",
- "Please provide a destination": "",
- "Please provide an amount greater than 0": "",
- "Please provide your existing seed": "",
- "Please provide your extended public key": "",
- "Please remove the NFC from the card reader": "",
- "Please review and confirm the transaction on your device...": "",
- "Please select a language": "",
- "Please select an option before proceeding": "",
- "Please set NBXPlorer's PostgreSQL connection string to make this feature available.": "",
- "Please verify that the address displayed on your device is <b>{0}</b>...": "",
- "Please wait for your node to be synched": "",
- "Please, confirm on the device first...": "",
- "Please, enter the passphrase on the device.": "",
- "Plugin action cancelled.": "",
- "Plugin scheduled to be enabled.": "",
- "Plugin scheduled to be installed.": "",
- "Plugin scheduled to be uninstalled.": "",
- "Plugin server": "",
- "Plugin update": "",
- "Plugin Updates": "",
- "Plugins": "",
- "Plugins are developed by third parties. They need to be updated and maintained regularly in addition to BTCPay Server. Use plugins at your own risk.": "",
- "Point of Sale": "",
- "Point of Sale Style": "",
- "Policies": "",
- "Policies updated successfully": "",
- "Port": "",
- "Powered by": "",
- "Preferred Price Source": "",
- "Prev": "",
- "Preview": "",
- "Price": "",
- "Price must be greater than 0": "",
- "Primary rate source": "",
- "Print": "",
- "Print display": "",
- "Private key or seed": "",
- "Pro tip: There are supported but unconfigured Payout Processors for this payout payment method.": "",
- "Proceed": "",
- "Proceed to free trial": "",
- "Proceed to Secure Payment": "",
- "Process approved payouts instantly": "",
- "Processing": "",
- "Processor updated.": "",
- "Product list": "",
- "Product list with cart": "",
- "Profile Picture": "",
- "Provide the 12 or 24 word recovery seed": "",
- "Provide updated PSBT": "",
- "Provider": "",
- "Prune old transactions from history": "",
- "PSBT content": "",
- "PSBT Successfully combined!": "",
- "PSBT to combine with…": "",
- "PSBT too large to be signed by Vault. (Max: {0} bytes)": "",
- "PSBT updated!": "",
- "Public Key": "",
- "Public keys successfully fetched.": "",
- "Public Node Info": "",
- "Pull payment archived": "",
- "Pull payment request created": "",
- "Pull payment updated successfully": "",
- "Pull Payments": "",
- "Pull Payments allow receivers to claim specified funds from your wallet at their convenience. Once submitted and approved, the funds will be released.": "",
- "Put these codes in a safe place": "",
- "QR Code connection": "",
- "QR Code data": "",
- "QR import failed: {0}": "",
- "Qty": "",
- "Query pairs via REST by querying {0} without the need to specify currencyPairs.": "",
- "Quick Fill": "",
- "Rate": "",
- "Rate rule scripting": "",
- "Rate Rules": "",
- "Rate rules scripting activated": "",
- "Rate rules scripting deactivated": "",
- "Rate script allows you to express precisely how you want to calculate rates for currency pairs.": "",
- "Rate settings updated": "",
- "Rate Source": "",
- "Rate Spread": "",
- "Rate unavailable: {0}": "",
- "Rates": "",
- "Re-enabling will not require you to reconfigure your app.": "",
- "Read more": "",
- "Receipt": "",
- "Receive": "",
- "Receive {0}": "",
- "Receive email notification updates.": "",
- "Receive updates for this invoice.": "",
- "Recent deliveries": "",
- "Recent Invoices": "",
- "Recent Transactions": "",
- "Recommendation ({0})": "",
- "Recommended": "",
- "Recommended fee confirmation target blocks": "",
- "Recovery Code": "",
- "Recovery codes": "",
- "Recurring": "",
- "Recurring Goal": "",
- "Recurring Type": "",
- "Redeliver": "",
- "Redirect invoiceWhy this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.