Skip to content

Commit

Permalink
Allow translations of BTCPay Server Backend by admins
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasDorier committed Jan 16, 2024
1 parent 5e25ee2 commit b0bd0f4
Show file tree
Hide file tree
Showing 28 changed files with 868 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using BTCPayServer.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace BTCPayServer.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20231219031609_translationsmigration")]
public partial class translationsmigration : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
if (migrationBuilder.IsNpgsql())
{
migrationBuilder.Sql("CREATE TABLE translations (key TEXT NOT NULL PRIMARY KEY, value TEXT NOT NULL)");
}
}

protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
127 changes: 124 additions & 3 deletions BTCPayServer.Tests/UtilitiesTests.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Amazon.Auth.AccessControlPolicy;
Expand All @@ -13,6 +15,10 @@
using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
using ExchangeSharp;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
Expand All @@ -22,18 +28,19 @@
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
using static BTCPayServer.Tests.TransifexClient;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.Extensions.FileSystemGlobbing;

namespace BTCPayServer.Tests
{
/// <summary>
/// This class hold easy to run utilities for dev time
/// </summary>
public class UtilitiesTests
public class UtilitiesTests : UnitTestBase
{
public ITestOutputHelper Logs { get; }

public UtilitiesTests(ITestOutputHelper logs)
public UtilitiesTests(ITestOutputHelper logs) : base(logs)
{
Logs = logs;
}
Expand Down Expand Up @@ -241,6 +248,120 @@ private void WaitCanWritePrompt(IWebDriver driver)
Thread.Sleep(200);
}

/// <summary>
/// This utilities crawl through the cs files in search for
/// Display attributes, then update Translations.Default to list them
/// </summary>
[Trait("Utilities", "Utilities")]
[Fact]
public async Task UpdateDefaultTranslations()
{
var soldir = TestUtils.TryGetSolutionDirectoryInfo();
List<string> defaultTranslatedKeys = new List<string>();

// Go through all cs files, and find [Display] and [DisplayName] attributes
foreach (var file in soldir.EnumerateFiles("*.cs", SearchOption.AllDirectories))
{
var txt = File.ReadAllText(file.FullName);
var tree = CSharpSyntaxTree.ParseText(txt, new CSharpParseOptions(LanguageVersion.Default));
var walker = new DisplayNameWalker();
walker.Visit(tree.GetRoot());
foreach (var k in walker.Keys)
{
defaultTranslatedKeys.Add(k);
}
}

// Go through all cshtml file, search for text-translate or ViewLocalizer usage
using (var tester = CreateServerTester())
{
await tester.StartAsync();
var engine = tester.PayTester.GetService<RazorProjectEngine>();
foreach (var file in soldir.EnumerateFiles("*.cshtml", SearchOption.AllDirectories))
{
var filePath = file.FullName;
var txt = File.ReadAllText(file.FullName);
if (txt.Contains("ViewLocalizer"))
{
var matches = Regex.Matches(txt, "ViewLocalizer\\[\"(.*?)\"\\]");
foreach (Match match in matches)
{
defaultTranslatedKeys.Add(match.Groups[1].Value);
}
}
else if (txt.Contains("text-translate"))
{
filePath = filePath.Replace(Path.Combine(soldir.FullName, "BTCPayServer"), "/");
var item = engine.FileSystem.GetItem(filePath);

var node = (DocumentIntermediateNode)engine.Process(item).Items[typeof(DocumentIntermediateNode)];
foreach (var n in node.FindDescendantNodes<TagHelperIntermediateNode>())
{
foreach (var tagHelper in n.TagHelpers)
{
if (tagHelper.Name.EndsWith("TranslateTagHelper"))
{
var htmlContent = n.FindDescendantNodes<HtmlContentIntermediateNode>().First();
var inner = txt.Substring(htmlContent.Source.Value.AbsoluteIndex, htmlContent.Source.Value.Length);
defaultTranslatedKeys.Add(inner);
}
}
}

}
}

}
defaultTranslatedKeys = defaultTranslatedKeys.Distinct().OrderBy(o => o).ToList();
var path = Path.Combine(soldir.FullName, "BTCPayServer/Services/Translations.Default.cs");
var defaultTranslation = File.ReadAllText(path);
var startIdx = defaultTranslation.IndexOf("\"\"\"");
var endIdx = defaultTranslation.LastIndexOf("\"\"\"");
var content = defaultTranslation.Substring(0, startIdx + 3);
content += "\n" + String.Join('\n', defaultTranslatedKeys) + "\n";
content += defaultTranslation.Substring(endIdx);
File.WriteAllText(path, content);
}
class DisplayNameWalker : CSharpSyntaxWalker
{
public List<string> Keys = new List<string>();
public bool InAttribute = false;
public override void VisitAttribute(AttributeSyntax node)
{
InAttribute = true;
base.VisitAttribute(node);
InAttribute = false;
}
public override void VisitIdentifierName(IdentifierNameSyntax node)
{
if (InAttribute)
{
InAttribute = node.Identifier.Text switch
{
"Display" => true,
"DisplayAttribute" => true,
"DisplayName" => true,
"DisplayNameAttribute" => true,
_ => false
};
}
}
public override void VisitAttributeArgument(AttributeArgumentSyntax node)
{
if (InAttribute)
{
var name = node.Expression switch
{
LiteralExpressionSyntax les => les.Token.ValueText,
IdentifierNameSyntax ins => ins.Identifier.Text,
_ => throw new InvalidOperationException("Unknown node")
};
Keys.Add(name);
InAttribute = false;
}
}
}

/// <summary>
/// This utility will make sure that permission documentation is properly written in swagger.template.json
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion BTCPayServer/BTCPayServer.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../Build/Version.csproj" Condition="Exists('../Build/Version.csproj')" />
<Import Project="../Build/Common.csproj" />

Expand Down Expand Up @@ -141,6 +141,7 @@
<Watch Remove="Views\Shared\LocalhostBrowserSupport.cshtml" />
<Watch Remove="Views\UIAccount\CheatPermissions.cshtml" />
<Watch Remove="Views\UIReports\StoreReports.cshtml" />
<Watch Remove="Views\UIServer\ServerTranslations.cshtml" />
<Content Update="Views\UIApps\_ViewImports.cshtml">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<Pack>$(IncludeRazorContentInPack)</Pack>
Expand Down
2 changes: 1 addition & 1 deletion BTCPayServer/Controllers/UIAccountController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
Expand Down Expand Up @@ -177,7 +178,6 @@ public async Task<IActionResult> Login(LoginViewModel model, string returnUrl =
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return View(model);
}

var fido2Devices = await _fido2Service.HasCredentials(user.Id);
var lnurlAuthCredentials = await _lnurlAuthService.HasCredentials(user.Id);
if (!await _userManager.IsLockedOutAsync(user) && (fido2Devices || lnurlAuthCredentials))
Expand Down
26 changes: 26 additions & 0 deletions BTCPayServer/Controllers/UIServerController.Translations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Models.ServerViewModels;
using BTCPayServer.Services;
using Microsoft.AspNetCore.Mvc;

namespace BTCPayServer.Controllers
{
public partial class UIServerController
{
[HttpGet("server/translations")]
public IActionResult ServerTranslations()
{
return View(new ServerTranslationsViewModel().SetTranslations(_localizer.Translations));
}
[HttpPost("server/translations")]
public async Task<IActionResult> ServerTranslations(ServerTranslationsViewModel viewModel)
{
var translation = Translations.CreateFromText(viewModel.Translations);
await _localizer.Save(translation);
TempData[WellKnownTempData.SuccessMessage] = "Translations updated";
return RedirectToAction();
}
}
}
5 changes: 4 additions & 1 deletion BTCPayServer/Controllers/UIServerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public partial class UIServerController : Controller
private readonly LinkGenerator _linkGenerator;
private readonly EmailSenderFactory _emailSenderFactory;
private readonly TransactionLinkProviders _transactionLinkProviders;
private readonly LocalizerService _localizer;

public UIServerController(
UserManager<ApplicationUser> userManager,
Expand All @@ -94,7 +95,8 @@ public partial class UIServerController : Controller
EmailSenderFactory emailSenderFactory,
IHostApplicationLifetime applicationLifetime,
IHtmlHelper html,
TransactionLinkProviders transactionLinkProviders
TransactionLinkProviders transactionLinkProviders,
LocalizerService localizer
)
{
_policiesSettings = policiesSettings;
Expand All @@ -120,6 +122,7 @@ TransactionLinkProviders transactionLinkProviders
ApplicationLifetime = applicationLifetime;
Html = html;
_transactionLinkProviders = transactionLinkProviders;
_localizer = localizer;
}

[Route("server/maintenance")]
Expand Down
10 changes: 10 additions & 0 deletions BTCPayServer/Hosting/BTCPayServerServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@
using NicolasDorier.RateLimits;
using Serilog;
using BTCPayServer.Services.Reporting;
using Microsoft.Extensions.Localization;
using Microsoft.AspNetCore.Mvc.Localization;


#if ALTCOINS
using BTCPayServer.Services.Altcoins.Monero;
using BTCPayServer.Services.Altcoins.Zcash;
Expand All @@ -88,6 +92,11 @@ public static IServiceCollection RegisterJsonConverter(this IServiceCollection s
}
public static IServiceCollection AddBTCPayServer(this IServiceCollection services, IConfiguration configuration, Logs logs)
{
services.TryAddSingleton<IStringLocalizerFactory, LocalizerFactory>();
services.TryAddSingleton<IHtmlLocalizerFactory, LocalizerFactory>();
services.TryAddSingleton<LocalizerService>();
services.TryAddSingleton<ViewLocalizer>();

services.AddSingleton<MvcNewtonsoftJsonOptions>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value);
services.AddSingleton<JsonSerializerSettings>(o => o.GetRequiredService<IOptions<MvcNewtonsoftJsonOptions>>().Value.SerializerSettings);
services.AddDbContext<ApplicationDbContext>((provider, o) =>
Expand Down Expand Up @@ -160,6 +169,7 @@ public static IServiceCollection AddBTCPayServer(this IServiceCollection service
AddSettingsAccessor<ThemeSettings>(services);
//
services.AddStartupTask<BlockExplorerLinkStartupTask>();
services.AddStartupTask<LoadTranslationsStartupTask>();
services.TryAddSingleton<InvoiceRepository>();
services.AddSingleton<PaymentService>();
services.AddSingleton<BTCPayServerEnvironment>();
Expand Down
17 changes: 17 additions & 0 deletions BTCPayServer/Hosting/LoadTranslationsStartupTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Services;

namespace BTCPayServer.Hosting
{
public class LoadTranslationsStartupTask(LocalizerService LocalizerService) : IStartupTask
{
public Task ExecuteAsync(CancellationToken cancellationToken = default)
{
// Do not make startup longer for this
_ = LocalizerService.Load();
return Task.CompletedTask;
}
}
}
2 changes: 2 additions & 0 deletions BTCPayServer/Hosting/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
Expand Down Expand Up @@ -167,6 +168,7 @@ public void ConfigureServices(IServiceCollection services)
.AddNewtonsoftJson()
.AddRazorRuntimeCompilation()
.AddPlugins(services, Configuration, LoggerFactory, bootstrapServiceProvider)
.AddDataAnnotationsLocalization()
.AddControllersAsServices();

services.AddServerSideBlazor();
Expand Down
1 change: 1 addition & 0 deletions BTCPayServer/Models/AccountViewModels/LoginViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class LoginViewModel

[Required]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
public string LoginCode { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System;
using BTCPayServer.Services;

namespace BTCPayServer.Models.ServerViewModels
{
public class ServerTranslationsViewModel
{
public string Translations { get; set; }
public int Lines { get; set; }

internal ServerTranslationsViewModel SetTranslations(Translations translations)
{
Translations = translations.ToTextFormat();
Lines = translations.Records.Count;
return this;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace BTCPayServer.Models.StoreViewModels
public class GeneralSettingsViewModel
{

[Display(Name = "Store ID")]
[Display(Name = "Store Id")]
public string Id { get; set; }

[Display(Name = "Store Name")]
Expand Down Expand Up @@ -51,7 +51,7 @@ public class GeneralSettingsViewModel
[Display(Name = "Add additional fee (network fee) to invoice …")]
public NetworkFeeMode NetworkFeeMode { get; set; }

[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
[Display(Name = "Consider the invoice paid even if the paid amount is % less than expected")]
[Range(0, 100)]
public double PaymentTolerance { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion BTCPayServer/Models/StoreViewModels/PaymentViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class PaymentViewModel
[Display(Name = "Add additional fee (network fee) to invoice …")]
public NetworkFeeMode NetworkFeeMode { get; set; }

[Display(Name = "Consider the invoice paid even if the paid amount is ... % less than expected")]
[Display(Name = "Consider the invoice paid even if the paid amount is % less than expected")]
[Range(0, 100)]
public double PaymentTolerance { get; set; }

Expand Down

0 comments on commit b0bd0f4

Please sign in to comment.