使用 JSON 语言资源进行本地化
在 ASP.NET Core 中,有几种不同的方法可以本地化/全球化我们的应用程序。选择适合你需求的方式非常重要。在这个例子中,你将看到我们如何制作一个多语言 ASP.NET Core 应用程序,该应用程序从 .json
文件中读取特定于语言的字符串并将其存储在内存中,以便在应用程序的所有部分提供本地化以及保持高性能。
我们这样做的方法是使用 Microsoft.EntityFrameworkCore.InMemory
包。
笔记:
- 此项目的命名空间是
DigitalShop
,你可以将其更改为项目自己的命名空间 - 考虑创建一个新项目,这样就不会遇到奇怪的错误
- 绝不是这个例子展示了最佳实践,所以如果你认为它可以改进,请善于编辑
首先,让我们将以下包添加到 project.json
文件中的现有 dependencies
部分:
"Microsoft.EntityFrameworkCore": "1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
"Microsoft.EntityFrameworkCore.InMemory": "1.0.0"
现在让我们用以下代码替换 Startup.cs
文件:(删除 using
语句,因为它们可以在以后轻松添加)
Startup.cs
namespace DigitalShop
{
public class Startup
{
public static string UiCulture;
public static string CultureDirection;
public static IStringLocalizer _e; // This is how we access language strings
public static IConfiguration LocalConfig;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) // this is where we store apps configuration including language
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
LocalConfig = Configuration;
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddViewLocalization().AddDataAnnotationsLocalization();
// IoC Container
// Add application services.
services.AddTransient<EFStringLocalizerFactory>();
services.AddSingleton<IConfiguration>(Configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, EFStringLocalizerFactory localizerFactory)
{
_e = localizerFactory.Create(null);
// a list of all available languages
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("fa-IR")
};
var requestLocalizationOptions = new RequestLocalizationOptions
{
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures,
};
requestLocalizationOptions.RequestCultureProviders.Insert(0, new JsonRequestCultureProvider());
app.UseRequestLocalization(requestLocalizationOptions);
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
public class JsonRequestCultureProvider : RequestCultureProvider
{
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var config = Startup.LocalConfig;
string culture = config["AppOptions:Culture"];
string uiCulture = config["AppOptions:UICulture"];
string culturedirection = config["AppOptions:CultureDirection"];
culture = culture ?? "fa-IR"; // Use the value defined in config files or the default value
uiCulture = uiCulture ?? culture;
Startup.UiCulture = uiCulture;
culturedirection = culturedirection ?? "rlt"; // rtl is set to be the default value in case culturedirection is null
Startup.CultureDirection = culturedirection;
return Task.FromResult(new ProviderCultureResult(culture, uiCulture));
}
}
}
在上面的代码中,我们首先添加三个 public static
字段变量,稍后我们将使用从设置文件中读取的值进行初始化。
在 Startup
类的构造函数中,我们将一个 json 设置文件添加到 builder
变量中。第一个文件是应用程序运行所必需的,因此如果项目根目录尚未存在,请继续在项目根目录中创建 appsettings.json
。使用 Visual Studio 2015,此文件是自动创建的,因此只需将其内容更改为:(如果不使用,可以省略 Logging
部分)
appsettings.json
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"AppOptions": {
"Culture": "en-US", // fa-IR for Persian
"UICulture": "en-US", // same as above
"CultureDirection": "ltr" // rtl for Persian/Arabic/Hebrew
}
}
继续,在项目根目录中创建三个文件夹:
Models
,Services
和 Languages
。在 Models
文件夹中创建另一个名为 Localization
的文件夹。
在 Services
文件夹中,我们创建了一个名为 EFLocalization
的新 .cs 文件。内容如下:(不包括 using
陈述)
EFLocalization.cs
namespace DigitalShop.Services
{
public class EFStringLocalizerFactory : IStringLocalizerFactory
{
private readonly LocalizationDbContext _db;
public EFStringLocalizerFactory()
{
_db = new LocalizationDbContext();
// Here we define all available languages to the app
// available languages are those that have a json and cs file in
// the Languages folder
_db.AddRange(
new Culture
{
Name = "en-US",
Resources = en_US.GetList()
},
new Culture
{
Name = "fa-IR",
Resources = fa_IR.GetList()
}
);
_db.SaveChanges();
}
public IStringLocalizer Create(Type resourceSource)
{
return new EFStringLocalizer(_db);
}
public IStringLocalizer Create(string baseName, string location)
{
return new EFStringLocalizer(_db);
}
}
public class EFStringLocalizer : IStringLocalizer
{
private readonly LocalizationDbContext _db;
public EFStringLocalizer(LocalizationDbContext db)
{
_db = db;
}
public LocalizedString this[string name]
{
get
{
var value = GetString(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = GetString(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null);
}
}
public IStringLocalizer WithCulture(CultureInfo culture)
{
CultureInfo.DefaultThreadCurrentCulture = culture;
return new EFStringLocalizer(_db);
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.Select(r => new LocalizedString(r.Key, r.Value, true));
}
private string GetString(string name)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.FirstOrDefault(r => r.Key == name)?.Value;
}
}
public class EFStringLocalizer<T> : IStringLocalizer<T>
{
private readonly LocalizationDbContext _db;
public EFStringLocalizer(LocalizationDbContext db)
{
_db = db;
}
public LocalizedString this[string name]
{
get
{
var value = GetString(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = GetString(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null);
}
}
public IStringLocalizer WithCulture(CultureInfo culture)
{
CultureInfo.DefaultThreadCurrentCulture = culture;
return new EFStringLocalizer(_db);
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.Select(r => new LocalizedString(r.Key, r.Value, true));
}
private string GetString(string name)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.FirstOrDefault(r => r.Key == name)?.Value;
}
}
}
在上面的文件中,我们实现了 Entity Framework Core 的 IStringLocalizerFactory
接口,以便创建自定义本地化服务。重要的部分是 EFStringLocalizerFactory
的构造函数,我们在其中列出所有可用语言并将其添加到数据库上下文中。这些语言文件中的每一个都充当单独的数据库。
现在将以下每个文件添加到 Models/Localization
文件夹:
Culture.cs
namespace DigitalShop.Models.Localization
{
public class Culture
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<Resource> Resources { get; set; }
}
}
Resource.cs
namespace DigitalShop.Models.Localization
{
public class Resource
{
public int Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public virtual Culture Culture { get; set; }
}
}
LocalizationDbContext.cs
namespace DigitalShop.Models.Localization
{
public class LocalizationDbContext : DbContext
{
public DbSet<Culture> Cultures { get; set; }
public DbSet<Resource> Resources { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase();
}
}
}
上述文件只是将填充语言资源,文化的模型,还有 EF Core 使用的典型 DBContext
。
我们需要做的最后一件事就是创建语言资源文件。JSON 文件用于存储应用中可用的不同语言的键值对。
在此示例中,我们的应用程序仅提供两种语言。英语和波斯语。对于每种语言,我们需要两个文件。包含键值对的 jSON 文件和包含与 JSON 文件同名的类的 .cs
文件。该类有一个方法,GetList
反序列化 JSON 文件并返回它。在我们之前创建的 EFStringLocalizerFactory
的构造函数中调用此方法。
因此,在 Languages
文件夹中创建这四个文件:
EN-US.cs
namespace DigitalShop.Languages
{
public static class en_US
{
public static List<Resource> GetList()
{
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
return JsonConvert.DeserializeObject<List<Resource>>(File.ReadAllText("Languages/en-US.json"), jsonSerializerSettings);
}
}
}
EN-US.json
[
{
"Key": "Welcome",
"Value": "Welcome"
},
{
"Key": "Hello",
"Value": "Hello"
},
]
FA-IR.cs
public static class fa_IR
{
public static List<Resource> GetList()
{
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
return JsonConvert.DeserializeObject<List<Resource>>(File.ReadAllText("Languages/fa-IR.json", Encoding.UTF8), jsonSerializerSettings);
}
}
FA-IR.json
[
{
"Key": "Welcome",
"Value": "خوش آمدید"
},
{
"Key": "Hello",
"Value": "سلام"
},
]
我们都完成了。现在,为了访问代码中任何位置的语言字符串(键值对)(.cs
或 .cshtml
),你可以执行以下操作:
在 .cs
文件中(无论是否为控制器,无所谓):
// Returns "Welcome" for en-US and "خوش آمدید" for fa-IR
var welcome = Startup._e["Welcome"];
在剃刀视图文件(.cshtml
)中:
<h1>@Startup._e["Welcome"]</h1>
要记住的几件事情:
- 如果你尝试访问 JSON 文件中不存在或加载的
Key
,你将获得密钥文字(在上面的示例中,尝试访问Startup._e["How are you"]
将返回How are you
,无论语言设置是否因为它不存在 - 如果更改语言
.json
文件中的字符串值,则需要重新启动应用程序。否则它只显示默认值(键名)。当你在没有调试的情况下运行应用程序时,这一点尤为重要。 appsettings.json
可用于存储你的应用可能需要的各种设置- 重新启动应用程序是没有必要的,如果你只是想改变从
appsettings.json
文件语言/文化设置。这意味着你可以在应用程序界面中选择一个选项,以便用户在运行时更改语言/文化。
这是最终的项目结构: