Hintergrundöversättningar Pt. 3
Inleiding
I tidigare artiklarna, har vi diskuterat vikten av översättningar i kontexten av webbapplikationer . Vi har också utforskat användandet av EasyNMT biblioteket för att utföra översättningarna i en ASP. NET Core-applikationM SK3 I denna artikel ska jag beskriva hur jag lagt till en bakgrundsservice till applikationen för att tillåta dig att skicka en översättningsansökningMSC5 som processeras i bakgrundenMska6
Igen, kan ni se alla källkod för detta på min GitHub sida.
Previous Articles
Här lägger vi till ett litet verktyg som skickar backround-arbeten till tjänsten som vi förklarade i del 2. Det här verktyget är en enkel form som tillåter dig att skicka en översättningsansökan till tjänsterna . Den ' är sedan kastad och tillförd till en kö som ger dig information om översättningens status
Det här lägger till en funktion där du kan översätta ett 'nyt' dokument

Den översättningskoden
T översättning
På vår markerdown-editor-sida lade jag in lite kod som innehåller en liten drop down (in _LanguageDropDown.cshtml) som låter dig välja språket du vill översätta till
@if (Model.IsNew)
{
var translationHidden = Model.TranslationTasks.Any() ? "" : "hidden";
<p class="text-blue-dark dark:text-blue-light inline-flex items-center justify-center space-x-2">
<partial name="_LanguageDropDown" for="Languages"/>
<button class="btn btn-outline btn-sm mt-1" x-on:click="window.mostlylucid.translations.submitTranslation"><i class='bx bx-send'></i>Translate</button>
</p>
<div id="translations" class="@translationHidden">
<partial name="_GetTranslations" model="Model.TranslationTasks" />
</div>
<div id="translatedcontent" class="hidden">
<textarea class="hidden" id="translatedcontentarea"></textarea>
</div>
}
_LanguageDropDown
Vår _LanguageDropDown partial view är en enkel dropdown som låter dig välja språket du vill översätta till. Det här är fyllt av en lista av språk i Languages egenskapen av modellen.
Ni kan se att den använder Alpine.js för att hantera dropdownen och för att sätta in det valriktade språket och flaggan som ska visas i huvudselektionsdelen.
Att använda Alping innebär att vi sparar minimalt, lokalt referensrat JavaScript i våra synpunkter. Det är ett bra sätt att hålla dina synpunkter rena och enkla att läsa
@using Mostlylucid.Helpers
@model List<string>
<div id="LanguageDropDown" x-data="{
open: false,
selectedLanguage: 'Select Language',
selectedFlag: '' ,
selectedShortCode:''
}" class="relative inline-block mt-3">
<!-- Dropdown Button -->
<button x-on:click="open = !open" class="btn btn-sm btn-outline flex items-center space-x-2">
<!-- Dynamically Show the Flag Icon -->
<template x-if="selectedFlag">
<img :src="selectedFlag" class="h-4 w-4 rounded outline outline-1 outline-green-dark dark:outline-white" alt="Selected Language Flag">
</template>
<span x-text="selectedLanguage"></span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- Dropdown Menu -->
<div x-show="open" x-on:click.away="open = false"
class="absolute left-0 mt-2 w-64 rounded-md shadow-lg dark:bg-custom-dark-bg bg-white ring-1 ring-black ring-opacity-5 z-50">
<ul class="p-2">
@foreach (var language in Model)
{
<li>
<a href="#"
x-on:click.prevent="selectedLanguage = '@(language.ConvertCodeToLanguage())'; selectedFlag = '/img/flags/@(language).svg'; selectedShortCode='@language'; open = false"
class="flex dark:text-white text-black items-center p-2 hover:bg-gray-100">
<img src="/img/flags/@(language).svg" asp-append-version="true" class="ml-2 h-4 w-4 mr-4 rounded outline outline-1 outline-green-dark dark:outline-white" alt="@language"> @language.ConvertCodeToLanguage()
</a>
</li>
}
</ul>
</div>
</div>
Senda översättning
Du kommer att se att det här har någon Apline- -.- -js-kod som låter oss window.mostlylucid.translations.submitTranslation funktion. Denna funktion är definierad i vår translations.js file som ingår i vår _Layout.cshtml file.
export function submitTranslation() {
const languageDropDown = document.getElementById('LanguageDropDown');
// Access Alpine.js data using Apline.$data (Alpine.js internal structure)
const alpineData = Alpine.$data(languageDropDown);
const shortCode = alpineData.selectedShortCode;
const markdown = simplemde.value();
if (shortCode === '' || markdown === '') return;
// Create the data object that matches your model
const model = {
Language: shortCode,
OriginalMarkdown: markdown
};
// Perform the fetch request to start the translation using POST
fetch('/api/translate/start-translation', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // The content type should be JSON
},
body: JSON.stringify(model) // Send the data object as JSON
})
.then(function(response) {
if (response.ok) {
// Process the returned task ID
return response.json(); // Parse the JSON response (assuming the task ID is returned in JSON)
} else {
console.error('Failed to start the translation');
}
})
.then(function(taskId) {
if (taskId) {
console.log("Task ID:", taskId);
// Trigger an HTMX request to get the translations after saving
htmx.ajax('get', "/editor/get-translations", {
target: '#translations', // Update this element with the response
swap: 'innerHTML', // Replace the content inside the target
}).then(function () {
// Remove the hidden class after the content is updated
document.getElementById('translations').classList.remove('hidden');
});
}
})
.catch(function(error) {
// Handle any errors that occur during the fetch
console.error('An error occurred:', error);
});
Sektionsdiagramm
Den här koden beskrivs i följande sekvensdiagrammen:
sequenceDiagram
autonumber
actor User
participant JS as Client JS
participant Alpine as Alpine.js
participant FetchAPI as Fetch API
participant HTMX as HTMX
User->>JS: Trigger submitTranslation()
JS->>Alpine: Access data from LanguageDropDown via Alpine.js
Alpine-->>JS: Return selectedShortCode and markdown
JS->>JS: Check if shortCode and markdown are not empty
alt shortCode or markdown is empty
JS->>User: Do nothing
else
JS->>FetchAPI: Send POST request with model {Language, OriginalMarkdown}
FetchAPI-->>JS: Return taskId in JSON response
alt response is ok
JS->>HTMX: Send GET request to /editor/get-translations via HTMX
HTMX-->>JS: Return updated translations
JS->>DOM: Remove 'hidden' class from translations element
else
JS->>Console: Log "Failed to start the translation"
end
end
JS->>Console: Handle any fetch errors
Även om det här ser ut som en massa kod.
Vi skickar en POST-fråga till servern med språket och markerdown innehållet. Det går till en slutpunkt som kallas
start-translationsom definieras i vårTranslationAPI. Det här börjar översättningsuppgiften och lägger den till kasken för den här användarenServern svarar med en uppgift ID ( som vi registrerar, men inte använder
Sedan skickar vi en GET- begäran till servern för att få ut översättningarna. Detta görs med hjälp av HTMX som är en bibliotek som tillåter dig att uppdatera delar av sidan utan att ladda upp hela sidan. Det här är ett mycket kraftfullt verktyg och används på många ställen i den här applikationen
Börja översättningens slutpunkt
Det här är en WebAPI-kontroller som tar uppgifter som innehåller markering och ett språkkode. Den skickar sedan in begäran till vår bakgrundsöversättningsserver , kastar upp uppgifterna igen tthe UserId ( som är inbegripda i en cookie ) och återlämnar uppgifternas ID till klienten M SK4 ( Jag deaktiverade attityden ValidateAntiForgeryToken för tillfället eftersom jag inte använder den
[HttpPost("start-translation")]
// [ValidateAntiForgeryToken]
public async Task<Results<Ok<string>, BadRequest<string>>> StartTranslation([FromBody] MarkdownTranslationModel model)
{
if(ModelState.IsValid == false)
{
return TypedResults.BadRequest("Invalid model");
}
if(!backgroundTranslateService.TranslationServiceUp)
{
return TypedResults.BadRequest("Translation service is down");
}
// Create a unique identifier for this translation task
var taskId = Guid.NewGuid().ToString("N");
var userId = Request.GetUserId(Response);
// Trigger translation and store the associated task
var translationTask = await backgroundTranslateService.Translate(model);
var translateTask = new TranslateTask(taskId, DateTime.Now, model.Language, translationTask);
translateCacheService.AddTask(userId, translateTask);
// Return the task ID to the client
return TypedResults.Ok(taskId);
}
Att få ut översättningar
Detta är begärt med hjälp av HTMX och återskapar översättningarna för den nuvarande användaren. Det här är en enkel slutpunkt som får översättningerna från cachet och återställer dem till klienten.
[HttpGet]
[Route("get-translations")]
public IActionResult GetTranslations()
{
var userId = Request.GetUserId(Response);
var tasks = translateCacheService.GetTasks(userId);
var translations = tasks.Select(x=> new TranslateResultTask(x, false)).ToList();
return PartialView("_GetTranslations", translations);
}
Att få en del av översättningar
Det här är en enkel bild som använder HTMX för att svara på servern varenda 5 sekund för att få ut översättningarna för den nuvarande användaren.
Det har också hantering när alla översättningar är färdiga för att stoppa röstsedeln ( genom att sätta ut aktiveringsknappen till none). Och att visa ett alternativ meddelande när det inte finns några översättningar
Jag använder också Humanizer biblioteket för att visa tiden som krävs för att komplettera översättningen i ett mänskligt läsbart format.
Detta resulterar i denna bild:

@using Humanizer
@using Mostlylucid.Helpers
@model List<Mostlylucid.MarkdownTranslator.Models.TranslateResultTask>
@{
var allCompleted = Model.All(x => x.Completed);
var noTranslations = Model.Count == 0;
var trigger = allCompleted ? "none" : "every 5s";
if (noTranslations)
{
<div class="alert alert-info" role="alert">
No translations have been requested yet.
</div>
}
else
{
<div class="translationpoller" hx-controller="Editor" hx-action="GetTranslations" hx-get hx-swap="outerHTML" hx-trigger="@trigger">
<table class="table">
<thead>
<th>
@Html.DisplayNameFor(model => model[0].TaskId)
</th>
<th>
@Html.DisplayNameFor(model => model[0].Completed)
</th>
<th >
@Html.DisplayNameFor(model => model[0].Language)
</th>
<th>
@Html.DisplayNameFor(model => model[0].TotalMilliseconds)
</th>
</thead>
@foreach (var item in Model)
{
<tr>
<td> <a href="#" x-on:click.prevent="window.mostlylucid.translations.viewTranslation('@item.TaskId')">View</a></td>
<td>@if (item.Completed)
{
<i class='bx bx-check text-green'></i>
}
else
{
<i class='bx bx-loader-alt animate-spin dark:text-white text-black'></i>
}
</td>
<td>
<p class="flex items-center">
<img src="/img/flags/@(item.Language).svg" asp-append-version="true" class="ml-2 h-4 w-4 mr-4 rounded outline outline-1 outline-green-dark dark:outline-white" alt="@item.Language">
@item.Language.ConvertCodeToLanguage()
</p>
</td>
<td>@(TimeSpan.FromMilliseconds(item.TotalMilliseconds).Humanize())</td>
</tr>
}
</table>
</div>
}
}
Den syn på översättningsfunktionen
Som ni ser i den här bilden kallar vi till en liten Alping-klick för att se översättningen.
<a href="#" x-on:click.prevent="window.mostlylucid.translations.viewTranslation('@item.TaskId')">View</a>
Vilket ropar in i detta. Allt det gör är att få upp den befolkade transaltionen från servern och visa den på sidan.
export function viewTranslation(taskId) {
// Construct the URL with the query parameters
const url = `/api/translate/get-translation/${taskId}`;
// Fetch call to the API endpoint
fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json' // Indicate that we expect a JSON response
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data =>
{
let translatedContentArea = document.getElementById("translatedcontent")
translatedContentArea.classList.remove("hidden");
let textArea = document.getElementById('translatedcontentarea');
textArea.classList.remove('hidden');
textArea.value = data.originalMarkdown;
simplemde.value(data.translatedMarkdown);
}) // Log the successful response data
.catch(error => console.error('Error:', error)); // Handle any errors
}
Att få översättnings slutpunkt
Detta är liknande den tidigare metoden för att få en lista på översättningar förutom att det får en enda översättning med OriginalMarkdown och TranslatedMarkdown befolkad:
[HttpGet]
[Route("get-translation/{taskId}")]
public Results<JsonHttpResult<TranslateResultTask>, BadRequest<string>> GetTranslation(string taskId)
{
var userId = Request.GetUserId(Response);
var tasks = translateCacheService.GetTasks(userId);
var translationTask = tasks.FirstOrDefault(t => t.TaskId == taskId);
if (translationTask == null) return TypedResults.BadRequest("Task not found");
var result = new TranslateResultTask(translationTask, true);
return TypedResults.Json(result);
}
I slutsats
Resultatet av allt detta är att du nu kan skicka ett översättnings begäran och se översättningens status i redaktören efter det.
Jag har mer som jag vill göra här, inklusive att göra re . "-", Render Markdown flow for the translated content etc , och så vidare. Igen, kan ni se alla källkod för detta på min GitHub sida. Så snälla gå och titta på det om ni vill se mer