diff --git a/BTCPayServer.Tests/Checkoutv2Tests.cs b/BTCPayServer.Tests/Checkoutv2Tests.cs
index dc101f6c3..66ab1f1b3 100644
--- a/BTCPayServer.Tests/Checkoutv2Tests.cs
+++ b/BTCPayServer.Tests/Checkoutv2Tests.cs
@@ -6,6 +6,7 @@ using BTCPayServer.Tests.Logging;
using BTCPayServer.Views.Stores;
using NBitcoin;
using OpenQA.Selenium;
+using OpenQA.Selenium.Support.UI;
using Xunit;
using Xunit.Abstractions;
@@ -308,6 +309,23 @@ namespace BTCPayServer.Tests
Assert.StartsWith("bitcoin:", payUrl);
Assert.Contains("&lightning=lnbcrt", payUrl);
s.Driver.FindElement(By.Id("PayByLNURL"));
+
+ // Language Switch
+ var languageSelect = new SelectElement(s.Driver.FindElement(By.Id("DefaultLang")));
+ Assert.Equal("English", languageSelect.SelectedOption.Text);
+ Assert.Equal("View Details", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
+ Assert.DoesNotContain("lang=", s.Driver.Url);
+ languageSelect.SelectByText("Deutsch");
+ Assert.Equal("Details anzeigen", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
+ Assert.Contains("lang=de", s.Driver.Url);
+
+ s.Driver.Navigate().Refresh();
+ languageSelect = new SelectElement(s.Driver.FindElement(By.Id("DefaultLang")));
+ Assert.Equal("Deutsch", languageSelect.SelectedOption.Text);
+ Assert.Equal("Details anzeigen", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
+ languageSelect.SelectByText("English");
+ Assert.Equal("View Details", s.Driver.FindElement(By.Id("DetailsToggle")).Text);
+ Assert.Contains("lang=en", s.Driver.Url);
}
[Fact(Timeout = TestTimeout)]
diff --git a/BTCPayServer.Tests/UtilitiesTests.cs b/BTCPayServer.Tests/UtilitiesTests.cs
index cd6782987..221538724 100644
--- a/BTCPayServer.Tests/UtilitiesTests.cs
+++ b/BTCPayServer.Tests/UtilitiesTests.cs
@@ -1,13 +1,27 @@
using System;
using System.Collections.Concurrent;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
+using System.Text;
+using System.Threading;
using System.Threading.Tasks;
+using Amazon.Auth.AccessControlPolicy;
+using Amazon.Runtime.Internal;
using BTCPayServer.Client;
+using BTCPayServer.Client.Models;
using BTCPayServer.Controllers;
+using ExchangeSharp;
+using NBitcoin;
using Newtonsoft.Json.Linq;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Chrome;
+using OpenQA.Selenium.DevTools.V100.Network;
+using OpenQA.Selenium.Support.UI;
using Xunit;
+using Xunit.Abstractions;
+using static BTCPayServer.Tests.TransifexClient;
namespace BTCPayServer.Tests
{
@@ -16,6 +30,12 @@ namespace BTCPayServer.Tests
///
public class UtilitiesTests
{
+ public ITestOutputHelper Logs { get; }
+
+ public UtilitiesTests(ITestOutputHelper logs)
+ {
+ Logs = logs;
+ }
internal static string GetSecuritySchemeDescription()
{
var description =
@@ -39,6 +59,174 @@ namespace BTCPayServer.Tests
string.Join("\n", storePolicies.Select(pair => $"* `{pair.Key}`: {pair.Value.Title}")));
return description;
}
+
+// ///
+// /// This will take the translations from v1 or v2
+// /// and upload them to transifex if not found
+// ///
+// [FactWithSecret("TransifexAPIToken")]
+// [Trait("Utilities", "Utilities")]
+//#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
+// public async Task UpdateTransifex()
+// {
+// // DO NOT RUN IT, THIS WILL ERASE THE CURRENT TRANSIFEX TRANSLATIONS
+
+// var client = GetTransifexClient();
+// var translations = JsonTranslation.GetTranslations(TranslationFolder.CheckoutV1);
+// var enTranslations = translations["en"];
+// translations.Remove("en");
+
+// foreach (var t in translations)
+// foreach (var w in t.Value.Words.ToArray())
+// {
+// if (w.Value == enTranslations.Words[w.Key])
+// t.Value.Words[w.Key] = null;
+// }
+// await client.UpdateTranslations(translations);
+// }
+//#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
+
+ /////
+ ///// This utility will copy translations made on checkout v1 to checkout v2
+ /////
+ //[Fact]
+ //[Trait("Utilities", "Utilities")]
+ //public void SetTranslationV1ToV2()
+ //{
+ // var mappings = new Dictionary();
+ // foreach (var kv in JsonTranslation.GetTranslations(TranslationFolder.CheckoutV1))
+ // {
+ // var v1File = kv.Value;
+ // var v2File = JsonTranslation.GetTranslation(TranslationFolder.CheckoutV2, v1File.Lang);
+ // if (mappings.Count == 0)
+ // {
+ // foreach (var prop1 in v1File.Words)
+ // foreach (var prop2 in v2File.Words)
+ // {
+ // if (Normalize(prop1.Key) == Normalize(prop2.Key))
+ // mappings.Add(prop1.Key, prop2.Key);
+ // }
+ // mappings.Add("Copied", "copy_confirm");
+ // mappings.Add("ConversionTab_BodyDesc", "conversion_body");
+ // mappings.Add("Return to StoreName", "return_to_store");
+ // }
+ // foreach (var m in mappings)
+ // {
+ // var orig = v1File.Words[m.Key];
+ // v2File.Words[m.Value] = orig;
+ // }
+ // v2File.Words["currentLanguage"] = v1File.Words["currentLanguage"];
+ // v2File.Save();
+ // }
+ //}
+
+ //private string Normalize(string name)
+ //{
+ // return name.Replace("_", "").ToLowerInvariant();
+ //}
+
+
+ ///
+ /// This utility will use selenium to pilot your browser to
+ /// automatically translate a language.
+ ///
+ /// Step 1: Close all Chrome instances
+ /// Step2: Edit "v1" variable if want to translate checkout v1 or v2
+ /// - Windows: "chrome.exe --remote-debugging-port=9222 https://chat.openai.com/"
+ /// - Linux: "google-chrome --remote-debugging-port=9222 https://chat.openai.com/"
+ /// Step 3: Run this.
+ ///
+ ///
+ [Trait("Utilities", "Utilities")]
+ [FactWithSecret("TransifexAPIToken")]
+ public async Task AutoTranslateChatGPT()
+ {
+ var file = TranslationFolder.CheckoutV1;
+
+ using var driver = new ChromeDriver(new ChromeOptions()
+ {
+ DebuggerAddress = "127.0.0.1:9222"
+ });
+
+ var englishTranslations = JsonTranslation.GetTranslation(file, "en");
+
+ TransifexClient client = GetTransifexClient();
+ var langs = await client.GetLangs(englishTranslations.TransifexProject, englishTranslations.TransifexResource);
+ foreach (var lang in langs)
+ {
+ if (lang == "en")
+ continue;
+ var jsonLangCode = GetLangCodeTransifexToJson(lang);
+ var v1LangFile = JsonTranslation.GetTranslation(TranslationFolder.CheckoutV1, jsonLangCode);
+
+ if (!v1LangFile.Exists())
+ continue;
+ var languageCurrent = v1LangFile.Words["currentLanguage"];
+ if (v1LangFile.ShouldSkip())
+ {
+ Logs.WriteLine("Skipped " + jsonLangCode);
+ continue;
+ }
+
+ var langFile = JsonTranslation.GetTranslation(file, jsonLangCode);
+ bool askedPrompt = false;
+ foreach (var translation in langFile.Words)
+ {
+ if (translation.Key == "NOTICE_WARN" ||
+ translation.Key == "currentLanguage" ||
+ translation.Key == "code")
+ continue;
+
+ var english = englishTranslations.Words[translation.Key];
+ if (translation.Value != null)
+ continue; // Already translated
+ if (!askedPrompt)
+ {
+ driver.FindElement(By.XPath("//a[contains(text(), \"New chat\")]")).Click();
+ Thread.Sleep(200);
+ var input = driver.FindElement(By.XPath("//textarea[@data-id]"));
+ input.SendKeys($"I am translating a checkout crypto payment page, and I want you to translate it from English (en-US) to {languageCurrent} ({jsonLangCode}).");
+ input.SendKeys(Keys.LeftShift + Keys.Enter);
+ input.SendKeys("Reply only with the translation of the sentences I will give you and nothing more." + Keys.Enter);
+ WaitCanWritePrompt(driver);
+ askedPrompt = true;
+ }
+ english = english.Replace('\n', ' ');
+ driver.FindElement(By.XPath("//textarea[@data-id]")).SendKeys(english + Keys.Enter);
+ WaitCanWritePrompt(driver);
+ var elements = driver.FindElements(By.XPath("//div[contains(@class,'markdown') and contains(@class,'prose')]//p"));
+ var result = elements.Last().Text;
+ langFile.Words[translation.Key] = result;
+ }
+ langFile.Save();
+ }
+ }
+
+ private static TransifexClient GetTransifexClient()
+ {
+ return new TransifexClient(FactWithSecretAttribute.GetFromSecrets("TransifexAPIToken"));
+ }
+
+ private void WaitCanWritePrompt(IWebDriver driver)
+ {
+
+retry:
+ Thread.Sleep(200);
+ try
+ {
+ driver.FindElement(By.XPath("//*[contains(text(), \"Regenerate response\")]"));
+ }
+ catch
+ {
+ goto retry;
+ }
+ Thread.Sleep(200);
+ }
+
+
+ ///
+ /// This utility will make sure that permission documentation is properly written in swagger.template.json
+ ///
[Trait("Utilities", "Utilities")]
[Fact]
public void UpdateSwagger()
@@ -50,7 +238,7 @@ namespace BTCPayServer.Tests
}
///
- /// Download transifex transactions and put them in BTCPayServer\wwwroot\locales
+ /// Download transifex transactions and put them in BTCPayServer\wwwroot\locales and BTCPayServer\wwwroot\locales\checkout
///
[FactWithSecret("TransifexAPIToken")]
[Trait("Utilities", "Utilities")]
@@ -58,77 +246,76 @@ namespace BTCPayServer.Tests
{
// 1. Generate an API Token on https://www.transifex.com/user/settings/api/
// 2. Run "dotnet user-secrets set TransifexAPIToken "
- var client = new TransifexClient(FactWithSecretAttribute.GetFromSecrets("TransifexAPIToken"));
- var proj = "o:btcpayserver:p:btcpayserver";
- var resource = $"{proj}:r:enjson";
- var json = await client.GetTransifexAsync($"https://rest.api.transifex.com/resource_language_stats?filter[project]={proj}&filter[resource]={resource}");
- var langs = json["data"].Select(o => o["id"].Value().Split(':').Last()).ToArray();
- json = await client.GetTransifexAsync($"https://rest.api.transifex.com/resource_strings?filter[resource]={resource}");
- var hashToKeys = json["data"]
- .ToDictionary(
- o => o["id"].Value().Split(':').Last(),
- o => o["attributes"]["key"].Value().Replace("\\.", "."));
+ await PullTransifexTranslationsCore(TranslationFolder.CheckoutV1);
+ await PullTransifexTranslationsCore(TranslationFolder.CheckoutV2);
- var translations = new ConcurrentDictionary();
- translations.TryAdd("en",
- new JObject(
- json["data"]
- .Select(o => new JProperty(
- o["attributes"]["key"].Value().Replace("\\.", "."),
- o["attributes"]["strings"]["other"].Value())))
- );
+ }
- var langsDir = Path.Combine(TestUtils.TryGetSolutionDirectoryInfo().FullName, "BTCPayServer", "wwwroot", "locales");
+ private async Task PullTransifexTranslationsCore(TranslationFolder folder)
+ {
+ var enTranslation = JsonTranslation.GetTranslation(folder, "en");
+ var client = GetTransifexClient();
+ var langs = await client.GetLangs(enTranslation.TransifexProject, enTranslation.TransifexResource);
+ var resourceStrings = await client.GetResourceStrings(enTranslation.TransifexResource);
+
+ enTranslation.Words.Clear();
+ enTranslation.Translate(resourceStrings.SourceTranslations);
+ enTranslation.Save();
Task.WaitAll(langs.Select(async l =>
{
if (l == "en")
return;
- retry:
- var j = await client.GetTransifexAsync($"https://rest.api.transifex.com/resource_translations?filter[resource]={resource}&filter[language]=l:{l}");
+retry:
try
{
- var jobj = new JObject(
- j["data"].Select(o => (Key: hashToKeys[o["id"].Value().Split(':')[^3]], Strings: o["attributes"]["strings"]))
- .Select(o =>
- new JProperty(
- o.Key,
- o.Strings.Type == JTokenType.Null ? translations["en"][o.Key].Value() : o.Strings["other"].Value()
- )));
- if (l == "ne_NP")
- l = "np_NP";
- if (l == "zh_CN")
- l = "zh-SP";
- if (l == "kk")
- l = "kk-KZ";
-
- var langCode = l.Replace("_", "-");
- jobj["code"] = langCode;
-
- if ((string)jobj["currentLanguage"] == "English")
- return; // Not translated
- if ((string)jobj["currentLanguage"] == "disable")
- return; // Not translated
-
- if (jobj["InvoiceExpired_Body_3"].Value() == translations["en"]["InvoiceExpired_Body_3"].Value())
+ var langCode = GetLangCodeTransifexToJson(l);
+ var langTranslations = await client.GetTranslations(resourceStrings, l);
+ var translation = JsonTranslation.GetTranslation(folder, langCode);
+ translation.Words.Clear();
+ translation.Translate(langTranslations);
+ if (translation.ShouldSkip())
{
- jobj["InvoiceExpired_Body_3"] = string.Empty;
+ Logs.WriteLine("Skipping " + langCode);
+ return;
}
- translations.TryAdd(langCode, jobj);
+
+ if (translation.Words.ContainsKey("InvoiceExpired_Body_3") && translation.Words["InvoiceExpired_Body_3"] == enTranslation.Words["InvoiceExpired_Body_3"])
+ {
+ translation.Words["InvoiceExpired_Body_3"] = string.Empty;
+ }
+ translation.Save();
}
catch
{
+ await Task.Delay(1000);
goto retry;
}
}).ToArray());
+ }
- foreach (var t in translations)
- {
- t.Value.AddFirst(new JProperty("NOTICE_WARN", "THIS CODE HAS BEEN AUTOMATICALLY GENERATED FROM TRANSIFEX, IF YOU WISH TO HELP TRANSLATION COME ON THE SLACK http://slack.btcpayserver.org TO REQUEST PERMISSION TO https://www.transifex.com/btcpayserver/btcpayserver/"));
- var content = t.Value.ToString(Newtonsoft.Json.Formatting.Indented);
- File.WriteAllText(Path.Combine(langsDir, $"{t.Key}.json"), content);
- }
+ internal static string GetLangCodeTransifexToJson(string l)
+ {
+ if (l == "ne_NP")
+ l = "np-NP";
+ if (l == "zh_CN")
+ l = "zh-SP";
+ if (l == "kk")
+ l = "kk-KZ";
+
+ return l.Replace("_", "-");
+ }
+ internal static string GetLangCodeJsonToTransifex(string l)
+ {
+ if (l == "np-NP")
+ l = "ne_NP";
+ if (l == "zh-SP")
+ l = "zh_CN";
+ if (l == "kk-KZ")
+ l = "kk";
+
+ return l.Replace("-", "_");
}
}
@@ -148,9 +335,254 @@ namespace BTCPayServer.Tests
var message = new HttpRequestMessage(HttpMethod.Get, uri);
message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", APIToken);
message.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/vnd.api+json"));
- var response = await Client.SendAsync(message);
+ using var response = await Client.SendAsync(message);
var str = await response.Content.ReadAsStringAsync();
return JObject.Parse(str);
}
+
+ public async Task UpdateTranslations(Dictionary translations)
+ {
+ var resourceStrings = await GetResourceStrings(translations.First().Value.TransifexResource);
+ List patches = new List();
+ List batches = new List();
+ foreach (var translation in translations.Values)
+ {
+ foreach (var word in translation.Words)
+ {
+ if (word.Key == "NOTICE_WARN")
+ continue;
+ patches.Add(new JObject()
+ {
+ ["id"] = $"{translation.TransifexResource}:s:{resourceStrings.KeyToHashMapping[word.Key]}:l:{UtilitiesTests.GetLangCodeJsonToTransifex(translation.Lang)}",
+ ["type"] = "resource_translations",
+ ["attributes"] = new JObject()
+ {
+ ["strings"] = word.Value is null ? null : new JObject()
+ {
+ ["other"] = word.Value
+ }
+ }
+ });
+ if (patches.Count >= 150)
+ {
+ batches.Add(patches.ToArray());
+ patches = new List();
+ }
+ }
+ if (patches.Count > 0)
+ {
+ batches.Add(patches.ToArray());
+ patches = new List();
+ }
+ }
+
+ if (patches.Count > 0)
+ {
+ batches.Add(patches.ToArray());
+ patches = new List();
+ }
+ await Task.WhenAll(batches.Select(async batch =>
+ {
+ var message = new HttpRequestMessage(HttpMethod.Get, "https://rest.api.transifex.com/resource_translations");
+ message.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", APIToken);
+ message.Method = HttpMethod.Patch;
+ var content = new StringContent(new JObject()
+ {
+ ["data"] = new JArray(batch.OfType