Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

using Microsoft.Extensions.Options;
using eFormAPI.Web.Infrastructure.Models;
using NUnit.Framework;
using eFormAPI.Web.Services;

Expand All @@ -34,7 +36,7 @@ public class TranslationServiceTests : DbTestFixture

public override void DoSetup()
{
_translationService = new TranslationService();
_translationService = new TranslationService(Options.Create(new GoogleTranslateOptions()));
}

[Test]
Expand Down
22 changes: 22 additions & 0 deletions eFormAPI/eFormAPI.Web.Tests/TranslationServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using eFormAPI.Web.Infrastructure.Models;
using NUnit.Framework;
using eFormAPI.Web.Services;

Expand All @@ -23,4 +26,23 @@ public void BuildTranslateRequest_NormalisesLocaleToIso_AndEncodesText()
Assert.That(encoded, Does.Contain("q=Tank+4"));
Assert.That(encoded, Does.Contain("target=de"));
}

[TestCase(null)]
[TestCase("")]
public async Task TranslateText_WithoutApiKey_IsNotConfigured_AndFailsWithoutCallingGoogle(string apiKey)
{
var service = new TranslationService(Options.Create(new GoogleTranslateOptions { ApiKey = apiKey }));

Assert.That(service.IsConfigured, Is.False);
var result = await service.TranslateText("Tank 4", "da", "en-US");
Assert.That(result.Success, Is.False);
}
Comment on lines +32 to +39

[Test]
public void IsConfigured_WithApiKey_IsTrue()
{
var service = new TranslationService(Options.Create(new GoogleTranslateOptions { ApiKey = "realkey" }));

Assert.That(service.IsConfigured, Is.True);
}
}
11 changes: 0 additions & 11 deletions eFormAPI/eFormAPI.Web/Abstractions/ITranslationService.cs

This file was deleted.

14 changes: 5 additions & 9 deletions eFormAPI/eFormAPI.Web/Controllers/TranslationController.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
using System.Threading.Tasks;
using eFormAPI.Web.Abstractions;
using eFormAPI.Web.Infrastructure.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Microting.eFormApi.BasePn.Abstractions.Translation;
using Microting.eFormApi.BasePn.Infrastructure.Models.API;

namespace eFormAPI.Web.Controllers;

[Authorize]
public class TranslationController(ITranslationService translationService, IOptions<GoogleTranslateOptions> options)
public class TranslationController(ITranslationService translationService)
: Controller
{
private readonly string _apiKey = options.Value.ApiKey;

[HttpGet]
[Route("api/get-translation")]
public async Task<OperationDataResult<string>> TranslateText(string sourceText, string sourceLanguageCode, string targetLanguageCode)
{
return await translationService.TranslateText(sourceText, sourceLanguageCode, targetLanguageCode, _apiKey);
return await translationService.TranslateText(sourceText, sourceLanguageCode, targetLanguageCode);
}

[HttpGet]
[Route("api/translation-possible")]
public OperationDataResult<bool> TranslateTextPossible()
{
return new OperationDataResult<bool>(true, !string.IsNullOrEmpty(_apiKey));
return new OperationDataResult<bool>(true, translationService.IsConfigured);
}

}
}
58 changes: 46 additions & 12 deletions eFormAPI/eFormAPI.Web/Services/TranslationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,70 @@
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
using eFormAPI.Web.Abstractions;
using eFormAPI.Web.Infrastructure.Models;
using Microsoft.Extensions.Options;
using Microting.eFormApi.BasePn.Abstractions.Translation;
using Microting.eFormApi.BasePn.Infrastructure.Models.API;

namespace eFormAPI.Web.Services;

public class TranslationService : ITranslationService
/// <summary>
/// Google Translate backed <see cref="ITranslationService"/>. The interface lives in
/// eFormApi.BasePn so plugins can translate server-side (#1384 in the
/// backendconfiguration plugin); the options, and so the API key, stay in the host.
/// Registered as a singleton: it holds only the key and a shared HttpClient.
/// </summary>
public class TranslationService(IOptions<GoogleTranslateOptions> options) : ITranslationService
{
public TranslationService()
{
// One client for the process (a client per call exhausts sockets); the pooled
// connections are recycled so a DNS change is picked up. The timeout bounds a save
// that waits on a translation.
private static readonly HttpClient Client =
new(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(5) })
{
Timeout = TimeSpan.FromSeconds(10)
};

}
private readonly string _apiKey = options.Value.ApiKey;

public bool IsConfigured => !string.IsNullOrEmpty(_apiKey);
Comment on lines +30 to +32

public async Task<OperationDataResult<string>> TranslateText(string sourceText, string sourceLanguageCode,
string targetLanguageCode, string apiKey)
string targetLanguageCode)
{
if (!IsConfigured)
{
return new OperationDataResult<string>(false, "Translate: no API key configured");
}

// E2E hook: a sentinel key returns deterministic fake text instead of
// calling Google. Gated behind ALLOW_FAKE_TRANSLATE (set only in CI) so
// it can NEVER fire in production, even if API_KEY were the sentinel.
if (apiKey == "FAKE_TRANSLATE_E2E"
if (_apiKey == "FAKE_TRANSLATE_E2E"
&& Environment.GetEnvironmentVariable("ALLOW_FAKE_TRANSLATE") == "true")
{
return new OperationDataResult<string>(true, "", $"[{targetLanguageCode}] {sourceText}");
}

var (apiUrl, form) = BuildTranslateRequest(sourceText, sourceLanguageCode, targetLanguageCode, apiKey);
var (apiUrl, form) = BuildTranslateRequest(sourceText, sourceLanguageCode, targetLanguageCode, _apiKey);

using HttpClient client = new HttpClient();
HttpResponseMessage response = await client.PostAsync(apiUrl, new FormUrlEncodedContent(form));
string responseBody = await response.Content.ReadAsStringAsync();
// The interface promises an unsuccessful result instead of an exception, so a
// network failure or the timeout never fails the caller's save.
try
{
using var response = await Client.PostAsync(apiUrl, new FormUrlEncodedContent(form));
var responseBody = await response.Content.ReadAsStringAsync();
return ToResult(response, responseBody);
}
catch (Exception e) when (e is HttpRequestException or TaskCanceledException or JsonException)
{
Console.WriteLine($"Translate error: {e.Message}");
return new OperationDataResult<string>(false, $"Translate failed: {e.Message}");
}
Comment on lines +61 to +65
}

private static OperationDataResult<string> ToResult(HttpResponseMessage response, string responseBody)
{
if (response.IsSuccessStatusCode)
{
var responseObject = JsonSerializer.Deserialize<TranslationResponse>(responseBody);
Expand Down Expand Up @@ -68,4 +102,4 @@ public static (string url, Dictionary<string, string> form) BuildTranslateReques
};
return (url, form);
}
}
}
3 changes: 2 additions & 1 deletion eFormAPI/eFormAPI.Web/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ namespace eFormAPI.Web;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Localization;
using Microting.eFormApi.BasePn.Abstractions;
using Microting.eFormApi.BasePn.Abstractions.Translation;
using Microting.eFormApi.BasePn.Infrastructure.Database.Entities;
using Microting.eFormApi.BasePn.Infrastructure.Models.Application;
using Microting.eFormApi.BasePn.Localization;
Expand Down Expand Up @@ -448,7 +449,7 @@ private static void ConnectServices(IServiceCollection services)
services.AddScoped<IEformCaseReportService, EformCaseReportService>();
services.AddScoped<IWordService, WordService>();
services.AddScoped<ITemplateVisualEditorService, EFormVisualEditorService>();
services.AddScoped<ITranslationService, TranslationService>();
services.AddSingleton<ITranslationService, TranslationService>();
services.AddScoped<ICmsService, CmsService>();
}

Expand Down
2 changes: 1 addition & 1 deletion eFormAPI/eFormAPI.Web/eFormAPI.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<PackageReference Include="Microsoft.Extensions.PlatformAbstractions" Version="1.1.0" />
<PackageReference Include="Microting.eForm" Version="10.0.41" />
<PackageReference Include="Microting.EformAngularFrontendBase" Version="10.0.40" />
<PackageReference Include="Microting.eFormApi.BasePn" Version="10.0.36" />
<PackageReference Include="Microting.eFormApi.BasePn" Version="10.0.37" />
<PackageReference Include="PureOtp" Version="1.0.0.1" />
<PackageReference Include="Sentry" Version="6.11.1" />
<PackageReference Include="Sentry.AspNetCore" Version="6.11.1" />
Expand Down
Loading