Support added for translations in the ASP.net razor views.
This commit is contained in:
parent
b9876b1d7b
commit
39a4f46b5b
@ -4,6 +4,7 @@ using e_suite.Service.Sentinel;
|
|||||||
using e_suite.UnitTestCore;
|
using e_suite.UnitTestCore;
|
||||||
using eSuite.API.Controllers;
|
using eSuite.API.Controllers;
|
||||||
using eSuite.API.SingleSignOn;
|
using eSuite.API.SingleSignOn;
|
||||||
|
using eSuite.API.Translation;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Moq;
|
using Moq;
|
||||||
@ -17,6 +18,8 @@ public abstract class AccountControllerTestBase : TestBase
|
|||||||
protected Mock<ISentinel> _sentinelMock = null!;
|
protected Mock<ISentinel> _sentinelMock = null!;
|
||||||
protected Mock<ISingleSignOn> _singleSignOnMock = null!;
|
protected Mock<ISingleSignOn> _singleSignOnMock = null!;
|
||||||
protected Mock<ICookieManager> _cookieManagerMock = null!;
|
protected Mock<ICookieManager> _cookieManagerMock = null!;
|
||||||
|
protected Mock<ITranslatorFactory> _translatorFactoryMock = null!;
|
||||||
|
protected Mock<ITranslator> _translatorMock = null!;
|
||||||
|
|
||||||
public override async Task Setup()
|
public override async Task Setup()
|
||||||
{
|
{
|
||||||
@ -27,7 +30,11 @@ public abstract class AccountControllerTestBase : TestBase
|
|||||||
_singleSignOnMock = new Mock<ISingleSignOn>();
|
_singleSignOnMock = new Mock<ISingleSignOn>();
|
||||||
_cookieManagerMock = new Mock<ICookieManager>();
|
_cookieManagerMock = new Mock<ICookieManager>();
|
||||||
|
|
||||||
_accountController = new AccountController(_userManagerMock.Object,_sentinelMock.Object, _singleSignOnMock.Object, _cookieManagerMock.Object);
|
_translatorMock = new Mock<ITranslator>();
|
||||||
|
_translatorFactoryMock = new Mock<ITranslatorFactory>();
|
||||||
|
_translatorFactoryMock.Setup(x => x.Use(It.IsAny<Namespaces>())).ReturnsAsync(_translatorMock.Object);
|
||||||
|
|
||||||
|
_accountController = new AccountController(_userManagerMock.Object,_sentinelMock.Object, _singleSignOnMock.Object, _cookieManagerMock.Object, _translatorFactoryMock.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void AddAuthorisedUserToController(long id, string email, string displayName)
|
protected void AddAuthorisedUserToController(long id, string email, string displayName)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using e_suite.API.Common.models;
|
using e_suite.API.Common.models;
|
||||||
|
using eSuite.API.Controllers;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Moq;
|
using Moq;
|
||||||
@ -45,9 +46,13 @@ public class ProfileGetUnitTests : AccountControllerTestBase
|
|||||||
Assert.That(response, Is.TypeOf<PartialViewResult>());
|
Assert.That(response, Is.TypeOf<PartialViewResult>());
|
||||||
var viewResult = response as PartialViewResult;
|
var viewResult = response as PartialViewResult;
|
||||||
Assert.That(viewResult?.ViewName, Is.EqualTo("Profile"));
|
Assert.That(viewResult?.ViewName, Is.EqualTo("Profile"));
|
||||||
Assert.That(viewResult?.Model, Is.TypeOf<Models.UserProfile>());
|
|
||||||
|
|
||||||
var actualProfile = viewResult?.Model as Models.UserProfile;
|
Assert.That(viewResult?.Model, Is.TypeOf<ProfileViewModel>());
|
||||||
|
var profileViewModel = viewResult!.Model as ProfileViewModel;
|
||||||
|
|
||||||
|
Assert.That(profileViewModel.Profile, Is.TypeOf<Models.UserProfile>());
|
||||||
|
|
||||||
|
var actualProfile = profileViewModel.Profile;
|
||||||
Assert.That(actualProfile, Is.Not.Null);
|
Assert.That(actualProfile, Is.Not.Null);
|
||||||
Assert.That(actualProfile?.Email, Is.EqualTo(userProfile.Email));
|
Assert.That(actualProfile?.Email, Is.EqualTo(userProfile.Email));
|
||||||
Assert.That(actualProfile?.FirstName, Is.EqualTo(userProfile.FirstName));
|
Assert.That(actualProfile?.FirstName, Is.EqualTo(userProfile.FirstName));
|
||||||
|
|||||||
@ -8,6 +8,7 @@ using e_suite.Service.Sentinel;
|
|||||||
using eSuite.API.Extensions;
|
using eSuite.API.Extensions;
|
||||||
using eSuite.API.security;
|
using eSuite.API.security;
|
||||||
using eSuite.API.SingleSignOn;
|
using eSuite.API.SingleSignOn;
|
||||||
|
using eSuite.API.Translation;
|
||||||
using eSuite.API.Utilities;
|
using eSuite.API.Utilities;
|
||||||
using eSuite.Core.Security;
|
using eSuite.Core.Security;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -15,6 +16,12 @@ using Microsoft.AspNetCore.Mvc;
|
|||||||
|
|
||||||
namespace eSuite.API.Controllers;
|
namespace eSuite.API.Controllers;
|
||||||
|
|
||||||
|
public class ProfileViewModel
|
||||||
|
{
|
||||||
|
public Models.UserProfile Profile { get; set; }
|
||||||
|
public ITranslatorFactory TranslatorFactory { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This MVC controller is used to support all user interactions when logging in and out of the system, including profile updates.
|
/// This MVC controller is used to support all user interactions when logging in and out of the system, including profile updates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -26,7 +33,7 @@ public class AccountController : ESuiteController
|
|||||||
private readonly ISentinel _sentinel;
|
private readonly ISentinel _sentinel;
|
||||||
private readonly ISingleSignOn _singleSignOn;
|
private readonly ISingleSignOn _singleSignOn;
|
||||||
private readonly ICookieManager _cookieManager;
|
private readonly ICookieManager _cookieManager;
|
||||||
|
private readonly ITranslatorFactory _translatorFactory;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default constructor used to inject dependencies
|
/// Default constructor used to inject dependencies
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -34,12 +41,13 @@ public class AccountController : ESuiteController
|
|||||||
/// <param name="sentinel"></param>
|
/// <param name="sentinel"></param>
|
||||||
/// <param name="singleSignOn"></param>
|
/// <param name="singleSignOn"></param>
|
||||||
/// <param name="cookieManager"></param>
|
/// <param name="cookieManager"></param>
|
||||||
public AccountController(IUserManager userManager, ISentinel sentinel, ISingleSignOn singleSignOn, ICookieManager cookieManager)
|
public AccountController(IUserManager userManager, ISentinel sentinel, ISingleSignOn singleSignOn, ICookieManager cookieManager, ITranslatorFactory translatorFactory)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_sentinel = sentinel;
|
_sentinel = sentinel;
|
||||||
_singleSignOn = singleSignOn;
|
_singleSignOn = singleSignOn;
|
||||||
_cookieManager = cookieManager;
|
_cookieManager = cookieManager;
|
||||||
|
_translatorFactory = translatorFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
private const string AccountProfileUrl = "~/account/profile";
|
private const string AccountProfileUrl = "~/account/profile";
|
||||||
@ -222,7 +230,7 @@ public class AccountController : ESuiteController
|
|||||||
|
|
||||||
return Ok();
|
return Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Page used for making alterations to a users profile.
|
/// Page used for making alterations to a users profile.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -234,9 +242,17 @@ public class AccountController : ESuiteController
|
|||||||
public async Task<IActionResult> ProfileGet(CancellationToken cancellationToken)
|
public async Task<IActionResult> ProfileGet(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var profile = await _userManager.GetProfile(User.Email(), cancellationToken);
|
var profile = await _userManager.GetProfile(User.Email(), cancellationToken);
|
||||||
var userProfile = profile.ToUserProfile();
|
Models.UserProfile userProfile = profile.ToUserProfile();
|
||||||
|
|
||||||
return PartialView("Profile", userProfile);
|
_translatorFactory.Locale = profile.PreferredLocale;
|
||||||
|
var profileViewModel = new ProfileViewModel
|
||||||
|
{
|
||||||
|
Profile = userProfile,
|
||||||
|
TranslatorFactory = _translatorFactory
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return PartialView("Profile", profileViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
using Autofac;
|
using Autofac;
|
||||||
using e_suite.DependencyInjection;
|
using e_suite.DependencyInjection;
|
||||||
using eSuite.API.SingleSignOn;
|
using eSuite.API.SingleSignOn;
|
||||||
using eSuite.Core.Clock;
|
using eSuite.API.Translation;
|
||||||
|
|
||||||
namespace eSuite.API.DependencyInjection;
|
namespace eSuite.API.DependencyInjection;
|
||||||
|
|
||||||
@ -23,6 +23,16 @@ internal class CoreRegistrationModule : ESuiteModule
|
|||||||
builder.RegisterType<CookieManager>().As<ICookieManager>();
|
builder.RegisterType<CookieManager>().As<ICookieManager>();
|
||||||
builder.RegisterType<HttpClientFacade>().As<IHttpClientFacade>();
|
builder.RegisterType<HttpClientFacade>().As<IHttpClientFacade>();
|
||||||
|
|
||||||
|
builder.RegisterType<JsonLocalizationService>()
|
||||||
|
.As<IJsonLocalizationService>()
|
||||||
|
.SingleInstance(); // safe to cache globally
|
||||||
|
builder.RegisterType<TranslatorFactory>()
|
||||||
|
.As<ITranslatorFactory>()
|
||||||
|
.InstancePerLifetimeScope(); // per-request, because locale is per-request
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//e_suite.Service.Mail.IocRegistration.RegisterTypes(builder);
|
//e_suite.Service.Mail.IocRegistration.RegisterTypes(builder);
|
||||||
//e_suite.Service.Sentinel.IocRegistration.RegisterTypes(builder);
|
//e_suite.Service.Sentinel.IocRegistration.RegisterTypes(builder);
|
||||||
|
|
||||||
|
|||||||
@ -6,8 +6,10 @@ using eSuite.API.DependencyInjection;
|
|||||||
using eSuite.API.HealthChecks;
|
using eSuite.API.HealthChecks;
|
||||||
using eSuite.API.Middleware;
|
using eSuite.API.Middleware;
|
||||||
using eSuite.API.Swagger;
|
using eSuite.API.Swagger;
|
||||||
|
using eSuite.API.Translation;
|
||||||
using HealthChecks.UI.Client;
|
using HealthChecks.UI.Client;
|
||||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
@ -49,6 +51,21 @@ builder.Services.AddHealthChecks()
|
|||||||
.AddCheck("Mail Server", () => SmtpHealthCheck.Healthy(builder.Configuration, new SocketFactory()));
|
.AddCheck("Mail Server", () => SmtpHealthCheck.Healthy(builder.Configuration, new SocketFactory()));
|
||||||
builder.Services.AddAntiforgery(options => options.HeaderName = "XSRF-TOKEN");
|
builder.Services.AddAntiforgery(options => options.HeaderName = "XSRF-TOKEN");
|
||||||
|
|
||||||
|
//builder.Services.Configure<FrontendSettings>(builder.Configuration);
|
||||||
|
//builder.Services.AddHttpClient<IJsonLocalizationService, JsonLocalizationService>()
|
||||||
|
// .ConfigureHttpClient((sp, client) =>
|
||||||
|
// {
|
||||||
|
// var settings = sp.GetRequiredService<IOptions<FrontendSettings>>().Value;
|
||||||
|
|
||||||
|
// // Ensure trailing slash
|
||||||
|
// var baseUrl = settings.BaseUrl.EndsWith("/")
|
||||||
|
// ? settings.BaseUrl
|
||||||
|
// : settings.BaseUrl + "/";
|
||||||
|
|
||||||
|
// client.BaseAddress = new Uri(baseUrl);
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
@ -85,3 +102,9 @@ app.UseMiddleware<SecurityAccessMiddleWare>();
|
|||||||
app.MapControllers().RequireAuthorization(); //This ensures that ALL API calls need a Bearer token, unless marked [AllowAnonymous] DO NOT REMOVE!
|
app.MapControllers().RequireAuthorization(); //This ensures that ALL API calls need a Bearer token, unless marked [AllowAnonymous] DO NOT REMOVE!
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
|
||||||
|
public class FrontendSettings
|
||||||
|
{
|
||||||
|
public string BaseUrl { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
217
e-suite.API/eSuite.API/Translation/TranslatorFactory.cs
Normal file
217
e-suite.API/eSuite.API/Translation/TranslatorFactory.cs
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
using eSuite.API.SingleSignOn;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using e_suite.API.Common.extensions;
|
||||||
|
|
||||||
|
namespace eSuite.API.Translation;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
|
public class NamespaceValueAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Value { get; }
|
||||||
|
|
||||||
|
public NamespaceValueAttribute(string value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Namespaces
|
||||||
|
{
|
||||||
|
[NamespaceValue("common")]
|
||||||
|
Common,
|
||||||
|
|
||||||
|
[NamespaceValue("mailTypes")]
|
||||||
|
MailTypes,
|
||||||
|
|
||||||
|
[NamespaceValue("htmlIsland")]
|
||||||
|
HtmlIsland
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NamespaceExtensions
|
||||||
|
{
|
||||||
|
public static string ToNamespaceString(this Namespaces ns)
|
||||||
|
{
|
||||||
|
var type = ns.GetType();
|
||||||
|
var member = type.GetMember(ns.ToString()).FirstOrDefault();
|
||||||
|
|
||||||
|
var attr = member?
|
||||||
|
.GetCustomAttributes(typeof(NamespaceValueAttribute), false)
|
||||||
|
.Cast<NamespaceValueAttribute>()
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
return attr?.Value ?? ns.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class I18nInterpolation
|
||||||
|
{
|
||||||
|
private static readonly Regex TokenRegex = new Regex(@"{{\s*(\w+)\s*}}",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
|
||||||
|
public static string Interpolate(string template, IDictionary<string, object> values)
|
||||||
|
{
|
||||||
|
if (template == null)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return TokenRegex.Replace(template, match =>
|
||||||
|
{
|
||||||
|
var key = match.Groups[1].Value;
|
||||||
|
|
||||||
|
if (values != null && values.TryGetValue(key, out var val))
|
||||||
|
return val?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
|
return match.Value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ITranslator
|
||||||
|
{
|
||||||
|
string T(string key, object values = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Translator : ITranslator
|
||||||
|
{
|
||||||
|
private readonly IDictionary<string, string> _translations;
|
||||||
|
|
||||||
|
public Translator(IDictionary<string, string> translations)
|
||||||
|
{
|
||||||
|
_translations = translations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string T(string key, object values = null)
|
||||||
|
{
|
||||||
|
if (!_translations.TryGetValue(key, out var template))
|
||||||
|
return key;
|
||||||
|
|
||||||
|
var dict = values?
|
||||||
|
.GetType()
|
||||||
|
.GetProperties()
|
||||||
|
.ToDictionary(p => p.Name, p => p.GetValue(values));
|
||||||
|
|
||||||
|
return I18nInterpolation.Interpolate(template, dict);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LocaleFallback
|
||||||
|
{
|
||||||
|
public static IEnumerable<string> ResolveFallbacks(string locale, string defaultLocale = "en")
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(locale))
|
||||||
|
{
|
||||||
|
var parts = locale.Split('-');
|
||||||
|
|
||||||
|
// Default first
|
||||||
|
if (!string.Equals(locale, defaultLocale, StringComparison.OrdinalIgnoreCase))
|
||||||
|
yield return defaultLocale;
|
||||||
|
|
||||||
|
// Language-only
|
||||||
|
if (parts.Length > 1)
|
||||||
|
yield return parts[0];
|
||||||
|
|
||||||
|
// Full locale last (most specific)
|
||||||
|
yield return locale;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
yield return defaultLocale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IJsonLocalizationService
|
||||||
|
{
|
||||||
|
Task<IDictionary<string, string>> LoadNamespaceAsync(string locale, string ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JsonLocalizationService : IJsonLocalizationService
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFacade _http;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly string _baseUrl;
|
||||||
|
|
||||||
|
public JsonLocalizationService(
|
||||||
|
IHttpClientFacade http,
|
||||||
|
IMemoryCache cache,
|
||||||
|
IConfiguration configuration
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_http = http;
|
||||||
|
_cache = cache;
|
||||||
|
|
||||||
|
_baseUrl = configuration.GetConfigValue("BASE_URL", "baseUrl", "http://localhost:3000");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IDictionary<string, string>> LoadNamespaceAsync(string locale, string ns)
|
||||||
|
{
|
||||||
|
var cacheKey = $"i18n:{locale}:{ns}";
|
||||||
|
|
||||||
|
return _cache.GetOrCreateAsync(cacheKey, async entry =>
|
||||||
|
{
|
||||||
|
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1);
|
||||||
|
return await LoadNamespaceInternalAsync(locale, ns);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IDictionary<string, string>> LoadNamespaceInternalAsync(string locale, string ns)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (var loc in LocaleFallback.ResolveFallbacks(locale))
|
||||||
|
{
|
||||||
|
var url = new Uri($"{_baseUrl}/locales/{loc}/{ns}.json");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var json = await _http.GetStringAsync(url, CancellationToken.None);
|
||||||
|
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||||
|
|
||||||
|
foreach (var kvp in dict)
|
||||||
|
result[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore missing files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface ITranslatorFactory
|
||||||
|
{
|
||||||
|
string Locale { get; set; }
|
||||||
|
Task<ITranslator> Use(params Namespaces[] namespaces);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TranslatorFactory : ITranslatorFactory
|
||||||
|
{
|
||||||
|
private readonly IJsonLocalizationService _localizer;
|
||||||
|
public string Locale { get; set; } = "en-GB";
|
||||||
|
|
||||||
|
public TranslatorFactory(IJsonLocalizationService localizer)
|
||||||
|
{
|
||||||
|
_localizer = localizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ITranslator> Use(params Namespaces[] namespaces)
|
||||||
|
{
|
||||||
|
var merged = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
foreach (var ns in namespaces)
|
||||||
|
{
|
||||||
|
var nsString = ns.ToNamespaceString();
|
||||||
|
var dict = await _localizer.LoadNamespaceAsync(Locale, nsString);
|
||||||
|
|
||||||
|
foreach (var kvp in dict)
|
||||||
|
merged[kvp.Key] = kvp.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Translator(merged);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,46 +1,45 @@
|
|||||||
@model eSuite.API.Models.UserProfile
|
@using eSuite.API.Translation
|
||||||
|
@model eSuite.API.Controllers.ProfileViewModel
|
||||||
|
|
||||||
@{
|
@{
|
||||||
ViewBag.Title = "e-suite";
|
var t = await Model.TranslatorFactory.Use(Namespaces.Common);
|
||||||
//Layout = "~/Views/Shared/_layout.cshtml";
|
|
||||||
Layout = null;
|
Layout = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1>Profile</h1>
|
|
||||||
@using (Html.BeginForm(FormMethod.Post, true, null))
|
@using (Html.BeginForm(FormMethod.Post, true, null))
|
||||||
{
|
{
|
||||||
@Html.AntiForgeryToken()
|
@Html.AntiForgeryToken()
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@Html.LabelFor(m => m.Email)
|
@Html.LabelFor(m => m.Profile.Email,t.T("Email"))
|
||||||
@Html.EditorFor(m => m.Email, new { htmlAttributes = new
|
@Html.EditorFor(m => m.Profile.Email, new { htmlAttributes = new
|
||||||
{
|
{
|
||||||
@class = "form-control",
|
@class = "form-control",
|
||||||
@disabled = "disabled"
|
@disabled = "disabled"
|
||||||
} })
|
} })
|
||||||
@Html.ValidationMessageFor(m => m.Email)
|
@Html.ValidationMessageFor(m => m.Profile.Email)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@Html.LabelFor(m => m.FirstName)
|
@Html.LabelFor(m => m.Profile.FirstName, t.T("FirstName"))
|
||||||
@Html.EditorFor(m => m.FirstName, new { htmlAttributes = new { @class = "form-control" } })
|
@Html.EditorFor(m => m.Profile.FirstName, new { htmlAttributes = new { @class = "form-control" } })
|
||||||
@Html.ValidationMessageFor(m => m.FirstName)
|
@Html.ValidationMessageFor(m => m.Profile.FirstName)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@Html.LabelFor(m => m.MiddleNames)
|
@Html.LabelFor(m => m.Profile.MiddleNames, t.T("MiddleNames"))
|
||||||
@Html.EditorFor(m => m.MiddleNames, new { htmlAttributes = new { @class = "form-control" } })
|
@Html.EditorFor(m => m.Profile.MiddleNames, new { htmlAttributes = new { @class = "form-control" } })
|
||||||
@Html.ValidationMessageFor(m => m.MiddleNames)
|
@Html.ValidationMessageFor(m => m.Profile.MiddleNames)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@Html.LabelFor(m => m.LastName)
|
@Html.LabelFor(m => m.Profile.LastName, t.T("LastName"))
|
||||||
@Html.EditorFor(m => m.LastName, new { htmlAttributes = new { @class = "form-control" } })
|
@Html.EditorFor(m => m.Profile.LastName, new { htmlAttributes = new { @class = "form-control" } })
|
||||||
@Html.ValidationMessageFor(m => m.LastName)
|
@Html.ValidationMessageFor(m => m.Profile.LastName)
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@await Html.PartialAsync("_loginMethodChooser")
|
@await Html.PartialAsync("_loginMethodChooser", Model.Profile)
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary btn-spaced" name="Submit">Save</button>
|
<button type="submit" class="btn btn-primary btn-spaced" name="Submit">@t.T("Save")</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -9,8 +9,8 @@
|
|||||||
</TestAncestor>
|
</TestAncestor>
|
||||||
</SessionState></s:String>
|
</SessionState></s:String>
|
||||||
<s:Boolean x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark1/@KeyIndexDefined">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark1/@KeyIndexDefined">True</s:Boolean>
|
||||||
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark1/Coords/@EntryValue">(Doc Ln 120 Col 0)</s:String>
|
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark1/Coords/@EntryValue">(Doc Ln 140 Col 8)</s:String>
|
||||||
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark1/FileId/@EntryValue">7DC1F493-76A5-3740-E774-C8DAA51ED83A/f:PatchUnitTests.cs</s:String>
|
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark1/FileId/@EntryValue">4A704FA7-4E3A-4CFA-B043-434A0C49AF89/d:Translation/f:TranslatorFactory.cs</s:String>
|
||||||
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark1/Owner/@EntryValue">NumberedBookmarkManager</s:String>
|
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark1/Owner/@EntryValue">NumberedBookmarkManager</s:String>
|
||||||
<s:Boolean x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark2/@KeyIndexDefined">True</s:Boolean>
|
<s:Boolean x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark2/@KeyIndexDefined">True</s:Boolean>
|
||||||
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark2/Coords/@EntryValue">(Doc Ln 606 Col 8)</s:String>
|
<s:String x:Key="/Default/Housekeeping/Bookmarks/NumberedBookmarks/=Bookmark2/Coords/@EntryValue">(Doc Ln 606 Col 8)</s:String>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user