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 Feb 5, 2024
1 parent 6437967 commit 7f76afc
Show file tree
Hide file tree
Showing 28 changed files with 866 additions and 19 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)
{
}
}
}
126 changes: 123 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,18 @@
using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
using static BTCPayServer.Tests.TransifexClient;
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 @@ -263,6 +269,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
1 change: 1 addition & 0 deletions BTCPayServer/BTCPayServer.csproj
Original file line number Diff line number Diff line change
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
1 change: 1 addition & 0 deletions 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
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 @@ -75,6 +75,10 @@
using BTCPayServer.Services.Reporting;
using BTCPayServer.Services.WalletFileParsing;

using Microsoft.Extensions.Localization;
using Microsoft.AspNetCore.Mvc.Localization;


#if ALTCOINS
using BTCPayServer.Services.Altcoins.Monero;
using BTCPayServer.Services.Altcoins.Zcash;
Expand All @@ -90,6 +94,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 @@ -165,6 +174,7 @@ public static IServiceCollection AddBTCPayServer(this IServiceCollection service
AddOnchainWalletParsers(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
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ namespace BTCPayServer.Models.WalletViewModels
public class WalletPSBTCombineViewModel
{
public string OtherPSBT { get; set; }
[Display(Name = "PSBT to combine with...")]
[Display(Name = "PSBT to combine with")]
public string PSBT { get; set; }
[Display(Name = "Upload PSBT from file...")]
[Display(Name = "Upload PSBT from file")]
public IFormFile UploadedPSBTFile { get; set; }

public string BackUrl { get; set; }
Expand Down

0 comments on commit 7f76afc

Please sign in to comment.