Added translation editor.
This commit is contained in:
@@ -22,6 +22,7 @@ namespace CarCareTracker.Controllers
|
|||||||
private readonly IExtraFieldDataAccess _extraFieldDataAccess;
|
private readonly IExtraFieldDataAccess _extraFieldDataAccess;
|
||||||
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
private readonly IReminderRecordDataAccess _reminderRecordDataAccess;
|
||||||
private readonly IReminderHelper _reminderHelper;
|
private readonly IReminderHelper _reminderHelper;
|
||||||
|
private readonly ITranslationHelper _translationHelper;
|
||||||
public HomeController(ILogger<HomeController> logger,
|
public HomeController(ILogger<HomeController> logger,
|
||||||
IVehicleDataAccess dataAccess,
|
IVehicleDataAccess dataAccess,
|
||||||
IUserLogic userLogic,
|
IUserLogic userLogic,
|
||||||
@@ -31,7 +32,8 @@ namespace CarCareTracker.Controllers
|
|||||||
IFileHelper fileHelper,
|
IFileHelper fileHelper,
|
||||||
IExtraFieldDataAccess extraFieldDataAccess,
|
IExtraFieldDataAccess extraFieldDataAccess,
|
||||||
IReminderRecordDataAccess reminderRecordDataAccess,
|
IReminderRecordDataAccess reminderRecordDataAccess,
|
||||||
IReminderHelper reminderHelper)
|
IReminderHelper reminderHelper,
|
||||||
|
ITranslationHelper translationHelper)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_dataAccess = dataAccess;
|
_dataAccess = dataAccess;
|
||||||
@@ -43,6 +45,7 @@ namespace CarCareTracker.Controllers
|
|||||||
_reminderHelper = reminderHelper;
|
_reminderHelper = reminderHelper;
|
||||||
_loginLogic = loginLogic;
|
_loginLogic = loginLogic;
|
||||||
_vehicleLogic = vehicleLogic;
|
_vehicleLogic = vehicleLogic;
|
||||||
|
_translationHelper = translationHelper;
|
||||||
}
|
}
|
||||||
private int GetUserID()
|
private int GetUserID()
|
||||||
{
|
{
|
||||||
@@ -260,6 +263,27 @@ namespace CarCareTracker.Controllers
|
|||||||
var userName = User.Identity.Name;
|
var userName = User.Identity.Name;
|
||||||
return PartialView("_RootAccountModal", new UserData() { UserName = userName });
|
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)]
|
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
|
||||||
public IActionResult Error()
|
public IActionResult Error()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using CarCareTracker.Models;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace CarCareTracker.Helper
|
namespace CarCareTracker.Helper
|
||||||
@@ -6,17 +7,22 @@ namespace CarCareTracker.Helper
|
|||||||
public interface ITranslationHelper
|
public interface ITranslationHelper
|
||||||
{
|
{
|
||||||
string Translate(string userLanguage, string text);
|
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
|
public class TranslationHelper : ITranslationHelper
|
||||||
{
|
{
|
||||||
private readonly IFileHelper _fileHelper;
|
private readonly IFileHelper _fileHelper;
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
|
private readonly ILogger<ITranslationHelper> _logger;
|
||||||
private IMemoryCache _cache;
|
private IMemoryCache _cache;
|
||||||
public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache)
|
public TranslationHelper(IFileHelper fileHelper, IConfiguration config, IMemoryCache memoryCache, ILogger<ITranslationHelper> logger)
|
||||||
{
|
{
|
||||||
_fileHelper = fileHelper;
|
_fileHelper = fileHelper;
|
||||||
_config = config;
|
_config = config;
|
||||||
_cache = memoryCache;
|
_cache = memoryCache;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
public string Translate(string userLanguage, string text)
|
public string Translate(string userLanguage, string text)
|
||||||
{
|
{
|
||||||
@@ -36,11 +42,13 @@ namespace CarCareTracker.Helper
|
|||||||
return translationDictionary ?? new Dictionary<string, string>();
|
return translationDictionary ?? new Dictionary<string, string>();
|
||||||
} catch (Exception ex)
|
} catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_logger.LogError(ex.Message);
|
||||||
return new Dictionary<string, string>();
|
return new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
_logger.LogError($"Could not find translation file for {userLanguage}");
|
||||||
return new Dictionary<string, string>();
|
return new Dictionary<string, string>();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -52,10 +60,113 @@ namespace CarCareTracker.Helper
|
|||||||
{
|
{
|
||||||
//create entry
|
//create entry
|
||||||
dictionary.Add(translationKey, text);
|
dictionary.Add(translationKey, text);
|
||||||
|
_logger.LogInformation($"Translation key added to {userLanguage} for {translationKey} with value {text}");
|
||||||
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(dictionary));
|
File.WriteAllText(translationFilePath, JsonSerializer.Serialize(dictionary));
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,12 +165,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<span class="lead">@translator.Translate(userLanguage, "Language")</span>
|
<span class="lead">@translator.Translate(userLanguage, "Language")</span>
|
||||||
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
|
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||||
@foreach (string uiLanguage in Model.UILanguages)
|
{
|
||||||
{
|
<div class="input-group">
|
||||||
<!option @(Model.UserConfig.UserLanguage == uiLanguage ? "selected" : "")>@uiLanguage</!option>
|
<select class="form-select" onchange="updateSettings()" id="defaultLanguage">
|
||||||
}
|
@foreach (string uiLanguage in Model.UILanguages)
|
||||||
</select>
|
{
|
||||||
|
<!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>
|
||||||
</div>
|
</div>
|
||||||
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
@if (User.IsInRole(nameof(UserData.IsRootUser)))
|
||||||
@@ -286,6 +302,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
function showReminderUrgencyThresholdModal(){
|
function showReminderUrgencyThresholdModal(){
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
|
|||||||
39
Views/Home/_TranslationEditor.cshtml
Normal file
39
Views/Home/_TranslationEditor.cshtml
Normal 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
@@ -163,4 +163,74 @@ function loadSponsors() {
|
|||||||
$.get('/Home/Sponsors', function (data) {
|
$.get('/Home/Sponsors', function (data) {
|
||||||
$("#sponsorsContainer").html(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;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user