使用 JSON 语言资源进行本地化

在 ASP.NET Core 中,有几种不同的方法可以本地化/全球化我们的应用程序。选择适合你需求的方式非常重要。在这个例子中,你将看到我们如何制作一个多语言 ASP.NET Core 应用程序,该应用程序从 .json 文件中读取特定于语言的字符串并将其存储在内存中,以便在应用程序的所有部分提供本地化以及保持高性能。

我们这样做的方法是使用 Microsoft.EntityFrameworkCore.InMemory 包。

笔记:

  1. 此项目的命名空间是 DigitalShop,你可以将其更改为项目自己的命名空间
  2. 考虑创建一个新项目,这样就不会遇到奇怪的错误
  3. 绝不是这个例子展示了最佳实践,所以如果你认为它可以改进,请善于编辑

首先,让我们将以下包添加到 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
  }
}

继续,在项目根目录中创建三个文件夹:

ModelsServicesLanguages。在 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 文件语言/文化设置。这意味着你可以在应用程序界面中选择一个选项,以便用户在运行时更改语言/文化。

这是最终的项目结构:

StackOverflow 文档