Added translation editor.

This commit is contained in:
DESKTOP-T0O5CDB\DESK-555BD 2024-10-25 11:06:40 -06:00
parent 5e3529f9cc
commit 090cbba1a0
6 changed files with 276 additions and 10 deletions

View File

@ -22,6 +22,7 @@ namespace CarCareTracker.Controllers
private readonly IExtraFieldDataAccess _extraFieldDataAccess;
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
private readonly IReminderHelper _reminderHelper;
private readonly ITranslationHelper _translationHelper;
public HomeController(ILogger<HomeController> logger,
IVehicleDataAccess dataAccess,
IUserLogic userLogic,
@ -31,7 +32,8 @@ namespace CarCareTracker.Controllers
IFileHelper fileHelper,
IExtraFieldDataAccess extraFieldDataAccess,
IReminderRecordDataAccess reminderRecordDataAccess,
IReminderHelper reminderHelper)
IReminderHelper reminderHelper,
ITranslationHelper translationHelper)
{
_logger = logger;
_dataAccess = dataAccess;
@ -43,6 +45,7 @@ namespace CarCareTracker.Controllers
_reminderHelper = reminderHelper;
_loginLogic = loginLogic;
_vehicleLogic = vehicleLogic;
_translationHelper = translationHelper;
}
private int GetUserID()
{
@ -260,6 +263,27 @@ namespace CarCareTracker.Controllers
var userName = User.Identity.Name;
return PartialView("_RootAccountModal", new UserData() { UserName = userName });
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpGet]
public IActionResult GetTranslatorEditor(string userLanguage)
{
var translationData = _translationHelper.GetTranslations(userLanguage);
return PartialView("_TranslationEditor", translationData);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult SaveTranslation(string userLanguage, Dictionary<string, string> translationData)
{
var result = _translationHelper.SaveTranslation(userLanguage, translationData);
return Json(result);
}
[Authorize(Roles = nameof(UserData.IsRootUser))]
[HttpPost]
public IActionResult ExportTranslation(Dictionary<string, string> translationData)
{
var result = _translationHelper.ExportTranslation(translationData);
return Json(result);
}
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.Caching.Memory;
using CarCareTracker.Models;
using Microsoft.Extensions.Caching.Memory;
using System.Text.Json;
namespace CarCareTracker.Helper
@ -6,17 +7,22 @@ namespace CarCareTracker.Helper
public interface ITranslationHelper
{
string Translate(string userLanguage, string text);
Dictionary<string, string> GetTranslations(string userLanguage);
OperationResponse SaveTranslation(string userLanguage, Dictionary<string, string> translations);
string ExportTranslation(Dictionary<string, string> translations);
}
public class TranslationHelper : ITranslationHelper
{
private readonly IFileHelper _fileHelper;
private readonly IConfiguration _config;
private readonly ILogger<ITranslationHelper> _logger;
private IMemoryCache _cache;
public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache)
public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache, ILogger<ITranslationHelper> logger)
{
_fileHelper = fileHelper;
_config = config;
_cache = memoryCache;
_logger = logger;
}
public string Translate(string userLanguage, string text)
{
@ -36,11 +42,13 @@ namespace CarCareTracker.Helper
return translationDictionary ?? new Dictionary<string, string>();
} catch (Exception ex)
{
_logger.LogError(ex.Message);
return new Dictionary<string, string>();
}
}
else
{
_logger.LogError($"Could not find translation file for {userLanguage}");
return new Dictionary<string, string>();
}
});
@ -52,10 +60,113 @@ namespace CarCareTracker.Helper
{
//create entry
dictionary.Add(translationKey, text);
_logger.LogInformation($"Translation key added to {userLanguage} for {translationKey} with value {text}");
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(dictionary));
return text;
}
return text;
}
private Dictionary<string, string> GetDefaultTranslation()
{
//this method always returns en_US translation.
var translationFilePath = _fileHelper.GetFullFilePath($"/defaults/en_US.json");
if (!string.IsNullOrWhiteSpace(translationFilePath))
{
//file exists.
try
{
var translationFile = File.ReadAllText(translationFilePath);
var translationDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(translationFile);
return translationDictionary ?? new Dictionary<string, string>();
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new Dictionary<string, string>();
}
}
_logger.LogError($"Could not find translation file for en_US");
return new Dictionary<string, string>();
}
public Dictionary<string, string> GetTranslations(string userLanguage)
{
var defaultTranslation = GetDefaultTranslation();
if (userLanguage == "en_US")
{
return defaultTranslation;
}
var translationFilePath = _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json");
if (!string.IsNullOrWhiteSpace(translationFilePath))
{
//file exists.
try
{
var translationFile = File.ReadAllText(translationFilePath);
var translationDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(translationFile);
if (translationDictionary != null)
{
foreach(var translation in translationDictionary)
{
defaultTranslation[translation.Key] = translation.Value;
}
}
return defaultTranslation ?? new Dictionary<string, string>();
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new Dictionary<string, string>();
}
}
_logger.LogError($"Could not find translation file for {userLanguage}");
return new Dictionary<string, string>();
}
public OperationResponse SaveTranslation(string userLanguage, Dictionary<string, string> translations)
{
if (userLanguage == "en_US")
{
return new OperationResponse { Success = false, Message = "The translation file name en_US is reserved." };
}
var translationFilePath = _fileHelper.GetFullFilePath($"/translations/{userLanguage}.json", false);
try
{
if (File.Exists(translationFilePath))
{
//write to file
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(translations));
_cache.Remove($"lang_{userLanguage}"); //clear out cache, force a reload from file.
} else
{
//write to file
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(translations));
}
return new OperationResponse { Success = true, Message = "Translation Updated" };
}
catch (Exception ex)
{
_logger.LogError(ex.Message);
return new OperationResponse { Success = false, Message = StaticHelper.GenericErrorMessage };
}
}
public string ExportTranslation(Dictionary<string, string> translations)
{
try
{
var tempFileName = $"/temp/{Guid.NewGuid()}.json";
string uploadDirectory = _fileHelper.GetFullFilePath("temp/", false);
if (!Directory.Exists(uploadDirectory))
{
Directory.CreateDirectory(uploadDirectory);
}
var saveFilePath = _fileHelper.GetFullFilePath(tempFileName, false);
File.WriteAllText(saveFilePath, JsonSerializer.Serialize(translations));
return tempFileName;
}
catch(Exception ex)
{
_logger.LogError(ex.Message);
return string.Empty;
}
}
}
}

View File

@ -165,12 +165,28 @@
</div>
<div class="col-12 col-md-6">
<span class="lead">@translator.Translate(userLanguage, "Language")</span>
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
@foreach (string uiLanguage in Model.UILanguages)
{
<!option @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@uiLanguage</!option>
}
</select>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
{
<div class="input-group">
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
@foreach (string uiLanguage in Model.UILanguages)
{
<!option @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@uiLanguage</!option>
}
</select>
<div class="input-group-text">
<button type="button" class="btn btn-sm btn-primary zero-y-padding" onclick="showTranslationEditor()"><i class="bi bi-pencil"></i></button>
</div>
</div>
} else
{
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
@foreach (string uiLanguage in Model.UILanguages)
{
<!option @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@uiLanguage</!option>
}
</select>
}
</div>
</div>
@if (User.IsInRole(nameof(UserData.IsRootUser)))
@ -286,6 +302,12 @@
</div>
</div>
</div>
<div class="modal fade" data-bs-focus="false" id="translationEditorModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content" id="translationEditorModalContent">
</div>
</div>
</div>
<script>
function showReminderUrgencyThresholdModal(){
Swal.fire({

View File

@ -0,0 +1,39 @@
@using CarCareTracker.Helper
@inject IConfigHelper config
@inject ITranslationHelper translator
@model Dictionary<string, string>
@{
var userConfig = config.GetUserConfig(User);
var userLanguage = userConfig.UserLanguage;
}
<div class="modal-header">
<h5 class="modal-title" id="translationEditorModalLabel">@translator.Translate(userLanguage, "Translation Editor")</h5>
<button type="button" class="btn-close" onclick="hideTranslationEditor()" aria-label="Close"></button>
</div>
<div class="modal-body" onkeydown="handleEnter(this)">
<form class="form-inline">
<div class="form-group">
@foreach(var translation in Model)
{
<div class="row translation-keyvalue mb-2">
<div class="col-md-6 col-12 translation-key">@translation.Key.Replace("_", " ")</div>
<div class="col-md-6 col-12 translation-value">
<textarea style="height:100%;" class="form-control" placeholder="@translation.Value">@translation.Value</textarea>
</div>
</div>
}
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="hideTranslationEditor()">@translator.Translate(userLanguage, "Cancel")</button>
<div class="btn-group">
<button type="button" onclick="saveTranslation()" class="btn btn-primary btn-md mt-1 mb-1">@translator.Translate(userLanguage, "Save Translation")</button>
<button type="button" class="btn btn-md btn-primary btn-md mt-1 mb-1 dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
<span class="visually-hidden">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" onclick="exportTranslation()">@translator.Translate(userLanguage, "Export Translation")</a></li>
</ul>
</div>
</div>

File diff suppressed because one or more lines are too long

View File

@ -163,4 +163,74 @@ function loadSponsors() {
$.get('/Home/Sponsors', function (data) {
$("#sponsorsContainer").html(data);
})
}
function showTranslationEditor() {
$.get(`/Home/GetTranslatorEditor?userLanguage=${$("#defaultLanguage").val()}`, function (data) {
$('#translationEditorModalContent').html(data);
$('#translationEditorModal').modal('show');
})
}
function hideTranslationEditor() {
$('#translationEditorModal').modal('hide');
}
function saveTranslation() {
var currentLanguage = $("#defaultLanguage").val();
var translationData = [];
$(".translation-keyvalue").map((index, elem) => {
var translationKey = $(elem).find('.translation-key');
var translationValue = $(elem).find('.translation-value textarea');
translationData.push({ key: translationKey.text().replaceAll(' ', '_').trim(), value: translationValue.val().trim() });
});
if (translationData.length == 0) {
errorToast(genericErrorMessage());
return;
}
Swal.fire({
title: 'Save Translation',
html: `
<input type="text" id="translationFileName" class="swal2-input" placeholder="Translation Name" value="${currentLanguage}" onkeydown="handleSwalEnter(event)">
`,
confirmButtonText: 'Save',
focusConfirm: false,
preConfirm: () => {
const translationFileName = $("#translationFileName").val();
if (!translationFileName || translationFileName.trim() == '') {
Swal.showValidationMessage(`Please enter a valid file name`);
} else if (translationFileName.trim() == 'en_US') {
Swal.showValidationMessage(`en_US is reserved, please enter a different name`);
}
return { translationFileName }
},
}).then(function (result) {
if (result.isConfirmed) {
$.post('/Home/SaveTranslation', { userLanguage: result.value.translationFileName, translationData: translationData }, function (data) {
if (data.success) {
successToast("Translation Updated");
updateSettings();
} else {
errorToast(genericErrorMessage());
}
});
}
});
}
function exportTranslation(){
var translationData = [];
$(".translation-keyvalue").map((index, elem) => {
var translationKey = $(elem).find('.translation-key');
var translationValue = $(elem).find('.translation-value textarea');
translationData.push({ key: translationKey.text().replaceAll(' ', '_').trim(), value: translationValue.val().trim() });
});
if (translationData.length == 0) {
errorToast(genericErrorMessage());
return;
}
$.post('/Home/ExportTranslation', { translationData: translationData }, function (data) {
if (!data) {
errorToast(genericErrorMessage());
} else {
window.location.href = data;
}
});
}