深度优先

这个家伙好懒,除了文章什么都没留下

0%

【.Net Core】使用SQL本地化的ASP.NET CORE

本文为Google翻译的 原文地址

本文介绍如何使用SQL数据库在ASP.NET Core中使用SQL本地化。 演示中的SQL本地化使用Entity Framework Core访问SQLite数据库。 可以将其配置为使用任何EF核心提供程序。

使用SQL本地化

要使用SQL本地化,需要将该库添加到project.json文件中的依赖项。 该库称为Localization.SqlLocalizer。

1
2
3
"dependencies": {
"Localization.SqlLocalizer": "2.0.0"
}

然后可以将其添加到Startup类的ConfigureServices方法中。 AddSqlLocalization方法要求在实体框架服务扩展中配置LocalizationModelContext类。 在此示例中,SQLite用作LocalizationModelContext上下文类的提供程序。 其余的本地化可以根据需要进行配置。 该示例支持en-US,de-CH,fr-CH,it-CH。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public void ConfigureServices(IServiceCollection services)
{
// services.AddLocalization(options => options.ResourcesPath = "Resources");

// init database for localization
var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

services.AddDbContext<LocalizationModelContext>(options =>
options.UseSqlite(
sqlConnectionString,
b => b.MigrationsAssembly("AspNet5Localization")
)
);

// Requires that LocalizationModelContext is defined
// services.AddSqlLocalization(options => options.UseTypeFullNames = true);
services.AddSqlLocalization();

services.AddMvc()
.AddViewLocalization()
.AddDataAnnotationsLocalization();

services.AddScoped<LanguageActionFilter>();

services.Configure<RequestLocalizationOptions>(
options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("de-CH"),
new CultureInfo("fr-CH"),
new CultureInfo("it-CH")
};

options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
}

还需要在Configure方法中添加本地化。

1
2
3
4
5
6
7
8
9
10
11
12
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
loggerFactory.AddDebug();

var locOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
app.UseRequestLocalization(locOptions.Value);

app.UseStaticFiles();

app.UseMvc();
}

现在需要创建数据库。 如果使用SQLite,则在 SqliteCreateLocalizationRecord.sql 文件中 提供创建sql脚本 。 如果使用其他数据库,则需要创建该数据库。 我没有提供迁移脚本。 可以使用SQLite Manager在Firefox中执行SQLite脚本。

1
2
3
4
5
6
7
8
CREATE TABLE "LocalizationRecords" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_DataEventRecord" PRIMARY KEY AUTOINCREMENT,
"Key" TEXT,
"ResourceKey" TEXT,
"Text" TEXT,
"LocalizationCulture" TEXT,
"UpdatedTimestamp" TEXT NOT NULL
)

该数据库现在应该存在。 我添加了一些基本的演示行。

SQL Localization的默认配置使用资源名称,然后使用资源密钥(如果遵循Microsoft的建议,则可能是默认语言文本),然后使用区域性。 这些属性中的每个属性在数据库中都有一个单独的字段。 如果找不到本地化,则返回搜索到的关键字。 这是一个未找到并返回到UI的本地化示例。

ASP.NET Core MVC控制器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace AspNet5Localization.Controllers
{

[ServiceFilter(typeof(LanguageActionFilter))]
[Route("api/{culture}/[controller]")]
public class AboutWithCultureInRouteController : Controller
{
// http://localhost:5000/api/it-CH/AboutWithCultureInRoute
// http://localhost:5000/api/fr-CH/AboutWithCultureInRoute

private readonly IStringLocalizer<SharedResource> _localizer;


public AboutWithCultureInRouteController(IStringLocalizer<SharedResource> localizer)
{
_localizer = localizer;
}

[HttpGet]
public string Get()
{
return _localizer["Name"];
}
}
}

使用的网址:

1
http://localhost:5000/api/fr-CH/AboutWithCultureInRoute

结果:ResoureKey:SharedResource,Key:名称,本地化文化:fr-CH

1
SharedResource.Name.fr-CH

这应该使查找和添加缺失的本地化变得容易。

配置SQL本地化

还可以将SQL Localization配置为使用不同的键在数据库中搜索本地化。 可以使用services.AddSqlLocalization并添加options参数在Startup类中进行配置。

SqlLocalizationOptions具有两个属性,UseTypeFullNames和UseOnlyPropertyNames。 如果UseOnlyPropertyNames为true,则在数据库中仅将属性名称用作带有ResourceKey全局键的键。 您还可以通过设置UseTypeFullNames将其配置为使用FullNames作为键。 如果设置了此名称,则数据库的ResourceKey属性中需要完整的类型名称。

1
2
3
4
5
6
7
8
9
10
11
12
public class SqlLocalizationOptions
{
/// <summary>
/// If UseOnlyPropertyNames is false, this property can be used to define keys with full type names or just the name of the class
/// </summary>
public bool UseTypeFullNames { get; set; }

/// <summary>
/// This can be used to use only property names to find the keys
/// </summary>
public bool UseOnlyPropertyNames { get; set; }
}

在Startup类中使用选项的示例:

1
2
3
4
5
6
7
8
9
var sqlConnectionString = Configuration["DbStringLocalizer:ConnectionString"];

services.AddEntityFramework()
.AddSqlite()
.AddDbContext<LocalizationModelContext>(
options => options.UseSqlite(sqlConnectionString));

// Requires that LocalizationModelContext is defined
services.AddSqlLocalization(options => options.UseTypeFullNames = true);

用于HTTP请求的Controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System.Globalization;
using System.Threading;

namespace AspNet5Localization.Controllers
{
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.Localization;

[Route("api/[controller]")]
public class AboutController : Controller
{
private readonly IStringLocalizer<SharedResource> _localizer;
private readonly IStringLocalizer<AboutController> _aboutLocalizerizer;

public AboutController(IStringLocalizer<SharedResource> localizer, IStringLocalizer<AboutController> aboutLocalizerizer)
{
_localizer = localizer;
_aboutLocalizerizer = aboutLocalizerizer;
}

[HttpGet]
public string Get()
{
// _localizer["Name"]
return _aboutLocalizerizer["AboutTitle"];
}
}
}

网址:

1
http://localhost:5000/api/about?culture=it-CH

结果:从结果中可以看到,SQL本地化使用FullName搜索了本地化。 ResoureKey:AspNet5Localization.Controllers.AboutController,键:AboutTitle,LocalizationCulture:it-CH

1
AspNet5Localization.Controllers.AboutController.AboutTitle.it-CH

SQL本地化详细

SQL本地化库使用扩展方法来提供其服务,该服务可在应用程序的Startup类中使用。 该库取决于Entity Framework Core。 任何数据库提供程序都可以用于此目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
using System;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Localization;

namespace Microsoft.Extensions.DependencyInjection
{
using global::Localization.SqlLocalizer;
using global::Localization.SqlLocalizer.DbStringLocalizer;

/// <summary>
/// Extension methods for adding localization servics to the DI container.
/// </summary>
public static class SqlLocalizationServiceCollectionExtensions
{
/// <summary>
/// Adds services required for application localization.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddSqlLocalization(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

return AddSqlLocalization(services, setupAction: null);
}

/// <summary>
/// Adds services required for application localization.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the services to.</param>
/// <param name="setupAction">An action to configure the <see cref="LocalizationOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddSqlLocalization(
this IServiceCollection services,
Action<SqlLocalizationOptions> setupAction)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}

services.TryAdd(new ServiceDescriptor(
typeof(IStringLocalizerFactory),
typeof(SqlStringLocalizerFactory),
ServiceLifetime.Singleton));
services.TryAdd(new ServiceDescriptor(
typeof(IStringLocalizer),
typeof(SqlStringLocalizer),
ServiceLifetime.Singleton));

if (setupAction != null)
{
services.Configure(setupAction);
}
return services;
}
}
}

SqlStringLocalizerFactory类实现IStringLocalizerFactory,该对象负责数据库访问。 该数据库仅用于每个资源的第一个请求。 这意味着此后会有更好的性能,但是新的翻译仅 应用程序重新启动 才能读取 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
namespace Localization.SqlLocalizer.DbStringLocalizer
{
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.PlatformAbstractions;

public class SqlStringLocalizerFactory : IStringLocalizerFactory
{
private readonly LocalizationModelContext _context;
private readonly ConcurrentDictionary<string, IStringLocalizer> _resourceLocalizations = new ConcurrentDictionary<string, IStringLocalizer>();
private readonly IOptions<SqlLocalizationOptions> _options;
private const string Global = "global";

public SqlStringLocalizerFactory(
LocalizationModelContext context,
IApplicationEnvironment applicationEnvironment,
IOptions<SqlLocalizationOptions> localizationOptions)
{
if (context == null)
{
throw new ArgumentNullException(nameof(LocalizationModelContext));
}

if (applicationEnvironment == null)
{
throw new ArgumentNullException(nameof(applicationEnvironment));
}

if (localizationOptions == null)
{
throw new ArgumentNullException(nameof(localizationOptions));
}

_options = localizationOptions;
_context = context;
}

public IStringLocalizer Create(Type resourceSource)
{
SqlStringLocalizer sqlStringLocalizer;

if (_options.Value.UseOnlyPropertyNames)
{
if (_resourceLocalizations.Keys.Contains(Global))
{
return _resourceLocalizations[Global];
}

sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(Global), Global);
return _resourceLocalizations.GetOrAdd(Global, sqlStringLocalizer);

}

if (_options.Value.UseTypeFullNames)
{
if (_resourceLocalizations.Keys.Contains(resourceSource.FullName))
{
return _resourceLocalizations[resourceSource.FullName];
}

sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(resourceSource.FullName), resourceSource.FullName);
_resourceLocalizations.GetOrAdd(resourceSource.FullName, sqlStringLocalizer);
}


if (_resourceLocalizations.Keys.Contains(resourceSource.Name))
{
return _resourceLocalizations[resourceSource.Name];
}

sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(resourceSource.Name), resourceSource.Name);
return _resourceLocalizations.GetOrAdd(resourceSource.Name, sqlStringLocalizer);
}

public IStringLocalizer Create(string baseName, string location)
{
if (_resourceLocalizations.Keys.Contains(baseName + location))
{
return _resourceLocalizations[baseName + location];
}

var sqlStringLocalizer = new SqlStringLocalizer(GetAllFromDatabaseForResource(baseName + location), baseName + location);
return _resourceLocalizations.GetOrAdd(baseName + location, sqlStringLocalizer);
}

private Dictionary<string, string> GetAllFromDatabaseForResource(string resourceKey)
{
return _context.LocalizationRecords.Where(data => data.ResourceKey == resourceKey).ToDictionary(kvp => (kvp.Key + "." + kvp.LocalizationCulture), kvp => kvp.Text);
}
}
}

SqlStringLocalizer实现IStringLocalizer。 因为它只是GET资源,所以用作应用程序的单例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
namespace Localization.SqlLocalizer.DbStringLocalizer
{
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.Extensions.Localization;

public class SqlStringLocalizer : IStringLocalizer
{
private readonly Dictionary<string, string> _localizations;

private readonly string _resourceKey;
public SqlStringLocalizer(Dictionary<string, string> localizations, string resourceKey)
{
_localizations = localizations;
_resourceKey = resourceKey;
}
public LocalizedString this[string name]
{
get
{
return new LocalizedString(name, GetText(name));
}
}

public LocalizedString this[string name, params object[] arguments]
{
get
{
return new LocalizedString(name, GetText(name));
}
}

public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
throw new NotImplementedException();
}

public IStringLocalizer WithCulture(CultureInfo culture)
{
throw new NotImplementedException();
}

private string GetText(string key)
{

#if NET451
var culture = System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
#elif NET46
var culture = System.Threading.Thread.CurrentThread.CurrentCulture.ToString();
#else
var culture = CultureInfo.CurrentCulture.ToString();
#endif
string computedKey = $"{key}.{culture}";

string result;
if (_localizations.TryGetValue(computedKey, out result))
{
return result;
}
else
{
return _resourceKey + "." + computedKey;
}
}
}
}

LocalizationModelContext类是实体框架核心DbContext实现。 这使用LocalizationRecord模型类进行数据访问。 影子属性用于数据库UpdatedTimestamp,在使用本地化数据库导入进行更新时将使用该属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
namespace Localization.SqlLocalizer.DbStringLocalizer
{
using System;
using System.Linq;

using Microsoft.EntityFrameworkCore;

// >dotnet ef migrations add LocalizationMigration
public class LocalizationModelContext : DbContext
{
public LocalizationModelContext(DbContextOptions<LocalizationModelContext> options) :base(options)
{ }

public DbSet<LocalizationRecord> LocalizationRecords { get; set; }

protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<LocalizationRecord>().HasKey(m => m.Id);
//builder.Entity<LocalizationRecord>().HasKey(m => m.LocalizationCulture + m.Key);

// shadow properties
builder.Entity<LocalizationRecord>().Property<DateTime>("UpdatedTimestamp");

base.OnModelCreating(builder);
}

public override int SaveChanges()
{
ChangeTracker.DetectChanges();
updateUpdatedProperty<LocalizationRecord>();
return base.SaveChanges();
}

private void updateUpdatedProperty<T>() where T : class
{
var modifiedSourceInfo =
ChangeTracker.Entries<T>()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);

foreach (var entry in modifiedSourceInfo)
{
entry.Property("UpdatedTimestamp").CurrentValue = DateTime.UtcNow;
}
}
}
}

下一步

该应用程序可以打包为NuGet并添加到NuGet服务器。 该数据库可以优化。 我还可以添加一些额外的配置选项。 我想为SPA json文件以及可以由外部公司翻译的csv文件实现导入,导出功能。

我很乐意提供反馈,非常感谢您提供改进方法的提示。 也许可以将其添加到ASP.NET Core中。