我将此答案发布为使用带有异步控制器的 ASP.NET MVC5 进行本地化的另一种自定义方式。也许您会在我的解决方案中发现一些问题,尤其是在路由和设置 cookie 方面。
这是我为我的异构/自定义方法草草写下的简短教程。所以我更喜欢 SO 而不是 WordPress。 :)
很抱歉没有对您的问题给出准确而离散的答案。希望它会以其他方式帮助您,以及其他人;谁正在寻求进行相同类型的设置。
在他的blog post 中,Nadeem Afana 描述了一种策略,即在解决方案中创建一个单独的项目Resource,以使用静态资源文件实现国际化。在blog sequel 中,他详细介绍了如何扩展同一项目以通过数据库和 XML 驱动的方法处理资源。对于前者,他使用了 ADO.NET,与 Entity Framework 解耦。
我们需要在 MVC 项目中实现静态和动态资源,同时尊重 MVC 约定的概念。
首先让我们在项目根目录中添加一个 Resources 文件夹,其中包含所需的语言变体:~/Resources/Resources.resx(默认资源文件对应于 en-US 文化)、~/Resources/Resources.fi.resx 和 ~/Resources/Resources.nl.resx。将资源标记为公开,以便在视图中提供它们。
在~/Views/Web.config 中,在<namespace> 元素下添加资源命名空间:<add namespace="YourMainNamespace.Reousrces" />。在控制器下,创建一个基本控制器类:
Cookie 来了
namespace YourNamespace.Controllers
{
// Don't forget to inherit other controllers with this
public class BaseController : Controller
{
protected override IAsyncResult BeginExecuteCore(AsyncCallback callback, object state)
{
string cultureName = null;
// Attempt to read the culture cookie from Request
HttpCookie cultureCookie = Request.Cookies["_culture"];
if (cultureCookie != null)
cultureName = cultureCookie.Value;
else
cultureName = Request.UserLanguages != null && Request.UserLanguages.Length > 0 ?
Request.UserLanguages[0] : // obtain it from HTTP header AcceptLanguages
null;
// Validate culture name
cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe
// Modify current thread's cultures
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
return base.BeginExecuteCore(callback, state);
}
}
}
接下来,在~/Global.asax.cs 中注册一个全局过滤器,以确保每个操作在执行前都应使用正确的区域性:
饼干又来了!
public class SetCultureActionFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var response = filterContext.RequestContext.HttpContext.Response;
var culture = filterContext.RouteData.Values["culture"].ToString();
// Validate input
culture = CultureHelper.GetImplementedCulture(culture);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(culture);
// Save culture in a cookie
HttpCookie cookie = filterContext.RequestContext.HttpContext.Request.Cookies["_culture"];
if (cookie != null)
cookie.Value = culture; // update cookie value
else
{
cookie = new HttpCookie("_culture");
cookie.Value = culture;
cookie.Expires = DateTime.Now.AddYears(1);
}
response.Cookies.Add(cookie);
}
}
并在MyApplication.Application_Start() 方法中添加GlobalFilters.Filters.Add(new SetCultureActionFilterAttribute());。
在~/App_Start/RoutesConfig.cs中,将默认路由改为:
routes.MapRoute(
name: "Default",
url: "{culture}/{controller}/{action}/{id}",
defaults: new { culture = "en-US", controller = "Home", action = "Index", id = UrlParameter.Optional }
);
此时,我们将能够在视图中使用资源。例如; @Resources.Headline.
接下来,我们将为模型属性创建一个名为 Translatable 的自定义属性。
class TranslatableAttribute : Attribute
{ }
这就够了。但是如果你希望能够指定范围,你可以使用这个类来实现它。
现在添加一个名为 Resource 的模型,它具有三个属性和一个辅助方法:
public class Resource
{
[Key, Column(Order = 0)]
public string Culture { get; set; }
[Key, Column(Order = 1)]
public string Name { get; set; }
public string Value { get; set; }
#region Helpers
// Probably using reflection not the best approach.
public static string GetPropertyValue<T>(string id, string propertyName) where T : class
{
return GetPropertyValue<T>(id, propertyName, Thread.CurrentThread.CurrentUICulture.Name);
}
public static string GetPropertyValue<T>(string id, string propertyName, string culture) where T : class
{
Type entityType = typeof(T);
string[] segments = propertyName.Split('.');
if (segments.Length > 1)
{
entityType = Type.GetType("YourNameSpace.Models." + segments[0]);
propertyName = segments[1];
}
if (entityType == null)
return "?<invalid type>";
var propertyInfo = entityType.GetProperty(propertyName);
var translateableAttribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true)
.FirstOrDefault();
/*var requiredAttribute = propertyInfo.GetCustomAttributes(typeof(RequiredAttribute), true)
.FirstOrDefault();*/
if (translateableAttribute == null)
return "?<this field has no translatable attribute>";
var dbCtx = new YourNamespaceDbContext();
var className = entityType.Name;
Resource resource = dbCtx.Resources.Where(r =>
(r.Culture == culture) &&
r.Name == className + id + propertyName).FirstOrDefault();
if (resource != null)
return resource.Value;
//return requiredAttribute == null ? string.Empty : "?<translation not found>";
return string.Empty;
}
#endregion
}
此辅助方法将帮助您检索已翻译的内容。比如在视图中,你可以说:
var name = Resource.GetPropertyValue<Product>(item.Id.ToString(), "Name");
请注意,在任何时候,可翻译字段列中的数据都是不可靠的;它将始终保存最后更新的值。在创建记录时,我们将在资源模型中为所有支持的文化镜像所有可翻译属性的值。
我们正在使用异步控制器,因此对于插入、修改和删除,我们将在 DbContext 类中覆盖 SaveChangesAsync():
public override Task<int> SaveChangesAsync()
{
ObjectContext ctx = ((IObjectContextAdapter)this).ObjectContext;
List<ObjectStateEntry> objectDeletedStateEntryList =
ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted)
.ToList();
List<ObjectStateEntry> objectCreateOrModifiedStateEntryList =
ctx.ObjectStateManager.GetObjectStateEntries(EntityState.Added
| EntityState.Modified)
.ToList();
// First handle the delition case,
// before making changes to entry state
bool changed = UpdateResources(objectDeletedStateEntryList);
// Now save the changes
int result = base.SaveChangesAsync().Result;
// Finally handle the remaining cases
changed |= UpdateResources(objectCreateOrModifiedStateEntryList);
if (changed)
return base.SaveChangesAsync();
return Task.FromResult<int>(result);
}
private bool UpdateResources(List<ObjectStateEntry> objectStateEntryList)
{
bool changed = false;
foreach (ObjectStateEntry entry in objectStateEntryList)
{
var typeName = entry.EntitySet.ElementType.Name;
if (entry.IsRelationship || typeName == "Resource")
return false;
var type = Type.GetType("YourNamespace.Models." + typeName);
if (type == null) // When seeds run (db created for the first-time), sometimes types might not be create
return false;
if (entry.State == EntityState.Deleted)
{
changed |= DeleteResources(type, typeName, entry);
continue;
}
foreach (var propertyInfo in type.GetProperties())
{
var attribute = propertyInfo.GetCustomAttributes(typeof(TranslatableAttribute), true).FirstOrDefault();
if (attribute == null)
continue;
CurrentValueRecord current = entry.CurrentValues;
object idField = current.GetValue(current.GetOrdinal("Id"));
if (idField == null)
continue;
var id = idField.ToString();
var propertyName = propertyInfo.Name;
string newValue = current.GetValue(current.GetOrdinal(propertyName)).ToString();
var name = typeName + id + propertyName;
Resource existingResource = this.Resources.Find(Thread.CurrentThread.CurrentUICulture.Name, name);
if (existingResource == null)
{
foreach (var culture in CultureHelper.Cultures)
{
this.Resources.Add(new Resource
{
Culture = culture,
Name = name,
Value = newValue
});
changed |= true;
}
}
else
{
existingResource.Value = newValue;
changed |= true;
}
}
}
return changed;
}
private bool DeleteResources(Type type, string typeName, ObjectStateEntry entry)
{
bool changed = false;
var firstKey = entry.EntityKey.EntityKeyValues.Where(k => k.Key.Equals("Id", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
if (firstKey == null)
return false;
var id = firstKey.Value.ToString();
foreach (var propertyInfo in type.GetProperties())
{
var name = typeName + id + propertyInfo.Name;
foreach (var culture in CultureHelper.Cultures)
{
Resource existingResource = this.Resources.Find(culture, name);
if (existingResource == null)
continue;
this.Resources.Remove(existingResource);
changed |= true;
}
}
return changed;
}
这将负责更新和删除。