I mentioned the code for a "database" solution
First, this approach uses a TagHelper that I have created for this.
how it works,
Step 1: The site renders the view with the tag helper.
Step 2: The tag helper gets noticed by the middleware and get's executed. The tag helper then goes to the injected repository and get's the right inner html.
for me the html would be.
<h5 language-key="CTrader-C1-H5">The C Trading with the C1 Algorithm</h5>
<p language-key="CTrader-C1">
The C1 Trader trades against the trend and allows you to..
</p>
Step 3: based on the users role I load JavaScript file that allows online editing of the pages text
Step 4: Update the template _ViewImports.cshtml to load the TagHelpers, in my case that would be:
@using CATS.Web.Shared.Repositories
@using CATS.Web.Shared.Infrastructure.TagHelpers
@using Microsoft.AspNetCore.Localization
@using Microsoft.AspNetCore.Mvc.Localization
@using Microsoft.AspNetCore.Identity
@addTagHelper "*, CATS.Web.Shared"
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
The Tag helper is defined like this:
[HtmlTargetElement("p",Attributes = CatsLanguageKey)]
[HtmlTargetElement("span", Attributes = CatsLanguageKey)]
[HtmlTargetElement("a", Attributes = CatsLanguageKey)]
[HtmlTargetElement("li", Attributes = CatsLanguageKey)]
[HtmlTargetElement("h1", Attributes = CatsLanguageKey)]
[HtmlTargetElement("h2", Attributes = CatsLanguageKey)]
[HtmlTargetElement("h3", Attributes = CatsLanguageKey)]
[HtmlTargetElement("h4", Attributes = CatsLanguageKey)]
[HtmlTargetElement("div", Attributes = CatsLanguageKey)]
public class LanguageTagHelper: TagHelper
{
private const string CatsLanguageKey= "language-key";
private readonly ILanguageRepository _repository;
private readonly ClaimsPrincipal _user;
private readonly IMemoryCache _memoryCache;
public LanguageTagHelper(ILanguageRepository repository, IHttpContextAccessor context, IMemoryCache memoryCache)
{
_repository = repository;
_user = context.HttpContext.User;
_memoryCache = memoryCache;
}
[HtmlAttributeName(CatsLanguageKey)]
public string Key { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
var childContent = await output.GetChildContentAsync();
if (!childContent.IsEmptyOrWhiteSpace)
{
var textItem = _repository.GetHtml(Key, childContent.GetContent().Trim());
if (_user.Identity.IsAuthenticated && _user.IsInRole(MagicStrings.ROLE_TEXTER))
{
output.Attributes.Add("data-language-target", textItem.Language);
output.Attributes.Add("data-language-key", textItem.Key);
var html = new HtmlString(textItem.Text);
output.Content.SetHtmlContent(html);
_memoryCache.Remove(Key);
}
else
{
string text = string.Empty;
if (!_memoryCache.TryGetValue(Key, out text))
{
text = Regex.Replace(textItem.Text, @">\s+<", "><", RegexOptions.Compiled | RegexOptions.Multiline);
text = Regex.Replace(text, @"<!--(?!\s*(?:\[if [^\]]+]|<!|>))(?:(?!-->)(.|\n))*-->", "", RegexOptions.Compiled | RegexOptions.Multiline);
text = Regex.Replace(text, @"^\s+", "", RegexOptions.Compiled | RegexOptions.Multiline);
text = Regex.Replace(text, @"\r\n?|\n", "", RegexOptions.Compiled | RegexOptions.Multiline);
text = Regex.Replace(text, @"\s+", " ", RegexOptions.Compiled | RegexOptions.Multiline);
_memoryCache.Set(Key, text, new MemoryCacheEntryOptions() { Priority= CacheItemPriority.Low, SlidingExpiration= new TimeSpan(hours:1,minutes:0,seconds:0) });
}
var html = new HtmlString(text);
output.Content.SetHtmlContent(html);
}
}
}
}
In the above code I also make sure I remove the cashed text incase the user is a texter else he would never see his updated text..
The java script that would open create and open the online text editor looks like this.
var languageUrl="";
var languageResetUrl = "";
function ResetPageText(key){
// var element = $("[data-language-key='" + key + "']");
// var key = element.data("language-key");
var args = {
__RequestVerificationToken: gettoken(),
textKey: key
};
$.post(languageResetUrl, args, function (data, textStatus, jqXHR) {
location.reload();
});
}
function SavePageText(key)
{
var element = $("[data-language-key='" + key + "']");
var lkey = element.data("language-key");
var language = element.data("language-target");
var text = $("#editor_" + lkey).val();
var model = {
__RequestVerificationToken: gt(),
textKey: key,
textLanguage: language,
textValue: $.trim(text),
salt: st()
};
if(text.length===0){
alert('Text did not contain any characters, translation not saved');
return;
}
$.ajax({
url: languageUrl,
method: 'POST',
data: model,
contentType: 'application/x-www-form-urlencoded',
headers: { 'X-XSRF-TOKEN': model.__RequestVerificationToken, 'X-Cats-Salt': model.salt },
success: function (data, textStatus, jqXHR) {
var sender = $("[data-language-key='" + data.key + "']");
sender.html(data.text);
$("#dlg").fadeOut('slow', function () {
$("#dlg").html("");
});
}
});
}
function UpdatePreview(key)
{
var preview = $("#2a" + key + "_pv");
preview.html($("#editor_" + key).val());
}
function ShowEditor(element)
{
var _key = $(element).data("language-key");
var _language = $(element).data("language-target");
var _value = $(element).html();
var _data = { Key: _key, Language: _language, Value: _value };
var form = [
"<div class='modal-container' id='d" + _key + ">"
, " <div class='modal-dialog'>"
, " <div class='modal-content'>"
, " <div class='modal-header'>"
, " <button type='button' class='close' onclick='CancelEdit(\"" + _key + "\")'><span class='white'>×</span></button>"
, " <h4 class='modal-title'>Update text element \"" + _key+"\" in language \"" + _language + "\"</h4>"
, " </div >"
, " <div class='modal-body'>"
, " <div id='exTab1' >"
, " <ul class='nav nav-pills'>"
, " <li class='active'><a href='#1a" + _key +"' data-toggle='tab'>HTML</a></li>"
, " <li><a href='#2a" + _key +"' data-toggle='tab' onclick='UpdatePreview(\""+_key+"\")'>Preview</a></li>"
," <ul>"
, " <div class='tab-content clearfix'>"
, " <div class='tab-pane active' id='1a" + _key+"'>"
, " <textarea class='editBox pad5' name='textValue' id='editor_" + _key + "' spellcheck='true' lang='" + _language + "'>" + _value + "</textarea >"
, " </div>"
, " <div class='tab-pane' id='2a" + _key + "'>"
, " <div class='preview' id='2a" + _key + "_pv'>"
, _value
, " </div>"
, " </div>"
, " </div>"
, " </div>"
, " <div class='modal-footer'>"
, " <button type='button' class='btn btn-default glyphicon glyphicon-repeat pad5' title='reset text to original' onclick='ResetPageText(\"" + _key + "\")'>Reset</button> "
, " <button type='button' class='btn btn-default glyphicon glyphicon-ok pad5' title='Save changes' onclick='SavePageText(\"" + _key + "\")'>Save</button>"
, " </div>"
, " </div>"
, " </div>"
,"</div>"
].join("\n");
$("#dlg").html(form);
$("#dlg").fadeIn();
}
function CancelEdit(id)
{
$("#dlg").fadeOut(400, 'swing', function () {
$("#dlg").html("");
});
}
Element.prototype.remove = function () {
this.parentElement.removeChild(this);
};
NodeList.prototype.remove = HTMLCollection.prototype.remove = function () {
for (var i = this.length - 1; i >= 0; i--) {
if (this[i] && this[i].parentElement) {
this[i].parentElement.removeChild(this[i]);
}
}
};
$(document).ready(function () {
$('[data-language-key]')
.on("click",
function (){
$(this).on("dblclick", ShowEditor(this));
});
});
I secure the JavaScript by adding "Salt" to my header ensuring that only the right user with the right salt and the right IP can update the site's text to avoid "3rd party" updates ;-)
I only load the script in the my _layour.cshtml shared page like this, mahig strings is just a class with magic strings like the once I use for role names when the user is in a specific role, hence he must be logged in before it gets activated.
<Roles app-role="@MagicStrings.ROLE_TEXTER">
<script src="~/js/Translate.js" type="text/javascript"></script>
<script>
languageUrl = '@Url.Action(action: "Update", controller: "PageText")';
languageResetUrl = '@Url.Action(action: "Reset", controller: "PageText")';
currentUrl = '@Context.Request.Path';
</script>
</Roles>
My Language is managed by the Language Repository. The code for this would be:
public class LanguageRepository : BaseRepository, ILanguageRepository
{
private readonly IMemoryCache _memoryCache;
private readonly IHttpContextAccessor _context;
public LanguageRepository(AppDbContext _db, IHttpContextAccessor context
, IMemoryCache memoryCache)
: base(_db)
{
_memoryCache = memoryCache;
_context = context;
}
private string Url
{
get {
return _context.HttpContext.Request.Path;
}
}
/// <summary>
/// Gets the language that the user is using.
/// </summary>
/// <value>
/// The language.
/// </value>
private string Language
{
get {
var code = System.Threading.Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName.ToLower();
if (new[] { "en", "de", "fr", "nl" }.Contains(code))
return code;
else
return "en";
}
}
public TextItem GetHtml(string key, string defaultIfNull)
{
var cashKey = string.Concat("tc_",Language ,"_", key);
if (!_memoryCache.TryGetValue<TextItem>(cashKey, out TextItem result))
{
result = new TextItem(key: key, language: Language);
using (var cmd = db.Database.GetDbConnection().CreateCommand())
{
if (cmd.Connection.State != System.Data.ConnectionState.Open)
cmd.Connection.Open();
cmd.CommandText = "dbo.GetPageText";
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@Key", System.Data.SqlDbType.VarChar) { Value = key, Size = 150 });
cmd.Parameters.Add(new SqlParameter("@Language", System.Data.SqlDbType.VarChar) { Value = Language, Size = 3 });
cmd.Parameters.Add(new SqlParameter("@DefaultIfNull", System.Data.SqlDbType.NVarChar) { Value = defaultIfNull, Size = 4000 });
cmd.Parameters.Add(new SqlParameter("@Url", System.Data.SqlDbType.VarChar) { Value = Url, Size = 150 });
result.Text = cmd.ExecuteScalar().ToString();
}
_memoryCache.Set<TextItem>(cashKey, result, MemoryOptions);
}
return result;
}
public bool SetHtml(string key, string value)
{
var cashKey = string.Concat("tc_", Language, "_", key);
TextItem result= new TextItem(key, Language) { Text= value };
_memoryCache.Set<TextItem>(cashKey, result, MemoryOptions);
using (var cmd = db.Database.GetDbConnection().CreateCommand())
{
if (cmd.Connection.State != System.Data.ConnectionState.Open)
cmd.Connection.Open();
cmd.CommandText = "dbo.SetPageText";
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@Key", System.Data.SqlDbType.VarChar) { Value = key, Size = 150 });
cmd.Parameters.Add(new SqlParameter("@Language", System.Data.SqlDbType.VarChar) { Value = Language, Size = 3 });
cmd.Parameters.Add(new SqlParameter("@Value", System.Data.SqlDbType.NVarChar) { Value = value, Size = 4000 });
return cmd.ExecuteNonQuery()!=0;
}
}
public async Task<bool> SetHtmlAsync(string key, string value)
{
var cashKey = string.Concat("tc_", Language, "_", key);
TextItem result= new TextItem(key, Language) { Text= value };
_memoryCache.Remove(cashKey);
_memoryCache.Set<TextItem>(cashKey, result, MemoryOptions);
using (var cmd = db.Database.GetDbConnection().CreateCommand())
{
if (cmd.Connection.State != System.Data.ConnectionState.Open)
cmd.Connection.Open();
cmd.CommandText = "dbo.SetPageText";
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@Key", System.Data.SqlDbType.VarChar) { Value = key, Size = 150 });
cmd.Parameters.Add(new SqlParameter("@Language", System.Data.SqlDbType.VarChar) { Value = Language, Size = 3 });
cmd.Parameters.Add(new SqlParameter("@Value", System.Data.SqlDbType.NVarChar) { Value = value, Size = 4000 });
return await cmd.ExecuteNonQueryAsync()!=0;
}
}
public async Task<bool> DeleteHtmlAsync(string key)
{
var cashKey = string.Concat("tc_", Language, "_", key);
_memoryCache.Remove(cashKey);
using (var cmd = db.Database.GetDbConnection().CreateCommand())
{
if (cmd.Connection.State != System.Data.ConnectionState.Open)
cmd.Connection.Open();
cmd.CommandText = "dbo.DeletePageText";
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.Parameters.Add(new SqlParameter("@Key", System.Data.SqlDbType.VarChar) { Value = key, Size = 150 });
return await cmd.ExecuteNonQueryAsync() != 0;
}
}
private MemoryCacheEntryOptions MemoryOptions=> new MemoryCacheEntryOptions() { Priority = CacheItemPriority.High, SlidingExpiration = DateTime.Now.AddHours(6) - DateTime.Now };
}