Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow translations of BTCPay Server Backend by admins #5662

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions BTCPayServer.Abstractions/Configuration/DataDirectories.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class DataDirectories
public string TempStorageDir { get; set; }
public string StorageDir { get; set; }
public string TempDir { get; set; }
public string LangsDir { get; set; }

public string ToDatadirFullPath(string path)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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 lang_dictionaries (
dict_id TEXT PRIMARY KEY,
fallback TEXT DEFAULT NULL,
source TEXT DEFAULT NULL,
metadata JSONB DEFAULT NULL,
FOREIGN KEY (fallback) REFERENCES lang_dictionaries(dict_id) ON UPDATE CASCADE ON DELETE SET NULL
);
INSERT INTO lang_dictionaries(dict_id, source) VALUES ('English', 'Default');

CREATE TABLE lang_translations (
dict_id TEXT NOT NULL,
sentence TEXT NOT NULL,
translation TEXT NOT NULL,
PRIMARY KEY (dict_id, sentence),
FOREIGN KEY (dict_id) REFERENCES lang_dictionaries(dict_id) ON UPDATE CASCADE ON DELETE CASCADE
);

CREATE VIEW translations AS
WITH RECURSIVE translations_with_paths AS (
SELECT d.dict_id, t.sentence, t.translation, ARRAY[d.dict_id] AS path FROM lang_translations t
INNER JOIN lang_dictionaries d USING (dict_id)

UNION ALL

SELECT d.dict_id, t.sentence, t.translation, d.dict_id || t.path FROM translations_with_paths t
INNER JOIN lang_dictionaries d ON d.fallback=t.dict_id
),
ranked_translations AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY dict_id, sentence ORDER BY array_length(path, 1)) AS rn
FROM translations_with_paths
)
SELECT dict_id, sentence, translation, path FROM ranked_translations WHERE rn=1;
COMMENT ON VIEW translations IS 'Compute the translation for all sentences for all dictionaries, taking into account fallbacks';
""");
}
}

protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}
8 changes: 8 additions & 0 deletions BTCPayServer.Tests/BTCPayServer.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@
<DefineConstants>$(DefineConstants);SHORT_TIMEOUT</DefineConstants>
</PropertyGroup>

<ItemGroup>
<None Remove="TestData\Langs\Cypherpunk" />
</ItemGroup>

<ItemGroup>
<Content Include="TestData\Langs\Cypherpunk" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Newtonsoft.Json.Schema" Version="3.0.15" />
Expand Down
11 changes: 9 additions & 2 deletions BTCPayServer.Tests/BTCPayServerTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Custodians;
using BTCPayServer.Configuration;
using BTCPayServer.HostedServices;
Expand Down Expand Up @@ -41,8 +42,7 @@ public enum TestDatabases

public class BTCPayServerTester : IDisposable
{
private readonly string _Directory;

internal readonly string _Directory;
public ILoggerProvider LoggerProvider { get; }

ILog TestLogs;
Expand Down Expand Up @@ -92,6 +92,13 @@ public TestDatabases TestDatabase
get; set;
}

public async Task RestartStartupTask<T>()
{
var startupTask = GetService<IServiceProvider>().GetServices<IStartupTask>()
.Single(task => task is T);
await startupTask.ExecuteAsync();
}

public bool MockRates { get; set; } = true;
public string SocksEndpoint { get; set; }

Expand Down
103 changes: 102 additions & 1 deletion BTCPayServer.Tests/LanguageServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Data;
using BTCPayServer.Hosting;
using BTCPayServer.Services;
using BTCPayServer.Tests.Logging;
using Dapper;
using Microsoft.EntityFrameworkCore;
using Xunit;
using Xunit.Abstractions;

Expand All @@ -14,6 +22,99 @@ public LanguageServiceTests(ITestOutputHelper helper) : base(helper)
{
}

[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanTranslateLoginPage()
{
using var tester = CreateSeleniumTester(newDb: true);
tester.Server.ActivateLangs();
await tester.StartAsync();
await tester.Server.PayTester.RestartStartupTask<LoadTranslationsStartupTask>();
}

[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanUpdateTranslationsInDatabase()
{
using var tester = CreateServerTester(newDb: true);
await tester.StartAsync();
var factory = tester.PayTester.GetService<ApplicationDbContextFactory>();
var db = factory.CreateContext().Database.GetDbConnection();

TestLogs.LogInformation("French fallback to english");
await db.ExecuteAsync("INSERT INTO lang_dictionaries VALUES ('English', NULL, NULL), ('French', 'English', NULL)");

Task translations_update(string dictId, (string Sentence, string Translation)[] translations)
{
return LocalizerService.translations_update(db, dictId, translations.Select(c => KeyValuePair.Create(c.Sentence, c.Translation)));
}
async Task AssertTranslations(string dictionary, (string Sentence, string Expected)[] expectations)
{
var all = await db.QueryAsync<(string sentence, string translation)>($"SELECT sentence, translation from translations WHERE dict_id='{dictionary}'");
foreach (var expectation in expectations)
{
if (expectation.Expected is not null)
Assert.Equal(expectation.Expected, all.Single(a => a.sentence == expectation.Sentence).translation);
else
Assert.DoesNotContain(all, a => a.sentence == expectation.Sentence);
}
}

await translations_update("English",
[
("Hello", "Hello"),
("Goodbye", "Goodbye"),
("Good afternoon", "Good afternoon")
]);
await translations_update("French",
[
("Hello", "Salut"),
("Good afternoon", "Bonne aprem")
]);

TestLogs.LogInformation("French should override Hello and Good afternoon, but not Goodbye");
await AssertTranslations("French",
[("Hello", "Salut"),
("Good afternoon", "Bonne aprem"),
("Goodbye", "Goodbye"),
("lol", null)]);
await AssertTranslations("English",
[("Hello", "Hello"),
("Good afternoon", "Good afternoon"),
("Goodbye", "Goodbye"),
("lol", null)]);

TestLogs.LogInformation("Can use fallback by setting null to a sentence");
await translations_update("French",
[
("Hello", null)
]);
await AssertTranslations("French",
[("Hello", "Hello"),
("Good afternoon", "Bonne aprem"),
("Goodbye", "Goodbye"),
("lol", null)]);

TestLogs.LogInformation("Can use fallback by setting same as fallback to a sentence");
await translations_update("French",
[
("Good afternoon", "Good afternoon")
]);
await AssertTranslations("French",
[("Hello", "Hello"),
("Good afternoon", "Good afternoon"),
("Goodbye", "Goodbye"),
("lol", null)]);

await translations_update("English", [("Hello", null as string)]);
await AssertTranslations("French",
[("Hello", null),
("Good afternoon", "Good afternoon"),
("Goodbye", "Goodbye"),
("lol", null)]);
await db.ExecuteAsync("DELETE FROM lang_dictionaries WHERE dict_id='English'");
}

[Fact(Timeout = TestTimeout)]
[Trait("Integration", "Integration")]
public async Task CanAutoDetectLanguage()
Expand Down
13 changes: 13 additions & 0 deletions BTCPayServer.Tests/ServerTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
using NBitcoin.RPC;
using NBitpayClient;
using NBXplorer;
using BTCPayServer.Abstractions.Contracts;
using System.Diagnostics.Metrics;

namespace BTCPayServer.Tests
{
Expand Down Expand Up @@ -72,6 +74,17 @@ public ServerTester(string scope, bool newDb, ILog testLogs, ILoggerProvider log
PayTester.SSHConnection = GetEnvironment("TESTS_SSHCONNECTION", "root@127.0.0.1:21622");
PayTester.SocksEndpoint = GetEnvironment("TESTS_SOCKSENDPOINT", "localhost:9050");
}

public void ActivateLangs()
{
TestLogs.LogInformation("Activating Langs...");
var dir = TestUtils.GetTestDataFullPath("Langs");
var langdir = Path.Combine(PayTester._Directory, "Langs");
Directory.CreateDirectory(langdir);
foreach (var file in Directory.GetFiles(dir))
File.Copy(file, Path.Combine(langdir, Path.GetFileName(file)));
}

#if ALTCOINS
public void ActivateLTC()
{
Expand Down
2 changes: 2 additions & 0 deletions BTCPayServer.Tests/TestData/Langs/Cypherpunk
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Password => Cyphercode
Email address => Cypher ID
4 changes: 1 addition & 3 deletions BTCPayServer.Tests/UnitTest1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2597,9 +2597,7 @@ private static async Task RestartMigration(ServerTester tester)
{
var settings = tester.PayTester.GetService<SettingsRepository>();
await settings.UpdateSetting<MigrationSettings>(new MigrationSettings());
var migrationStartupTask = tester.PayTester.GetService<IServiceProvider>().GetServices<IStartupTask>()
.Single(task => task is MigrationStartupTask);
await migrationStartupTask.ExecuteAsync();
await tester.PayTester.RestartStartupTask<MigrationStartupTask>();
}

[Fact(Timeout = LongRunningTestTimeout)]
Expand Down