.NET REST API的数据驱动本地化
目录
介绍
由于云的高度自动化,软件模型和数据变得越来越动态。让我们以一家在线水果店为例,该商店在多个国家/地区销售其产品。新产品由用户以不同的语言输入。
使用本地化模型数据设计REST API时,必须考虑以下注意事项
- 需要支持哪些语言?
- 哪些数据需要本地化(文本、数字、图像等)?
- 管理本地化对象需要哪些端点?
- 哪些端点将提供本地化数据?
- 如何在关系数据库系统中管理本地化数据?
可扩展的REST API将信息分离为可翻译和可读的数据。模型数据管理终结点包含所有本地化数据。在在线商店终结点中,数据以特定语言提供。这样可以节省资源,并且通常可以确保并非所有信息都以所有语言分发。
除了数据本地化之外,对于依赖区域性设置来转换数字、货币和日期数据的REST API来说,以下注意事项也很重要。
REST API产品本地化:
- REST客户端标识可用的区域性。
- 产品以多种语言捕获和提交。
- REST客户端以查询字符串、HTTP请求标头或cookie的形式请求特定语言的产品。
- REST API返回本地化的产品数据。
NuGet包
若要使用此处所述的本地化功能,必须安装NuGet包 RestApiLocalization.NET。该软件包包括对支持的系统区域性(Culture Provider)的管理以及用于本地化数据的扩展方法。
文化——本地化基础
根据 RFC4646/ISO639/ISO3166 的规则,NET区域性具有唯一的名称,该规则定义了用于确定区域性的三个级别:
Windows系统中注册的语言可以根据以下条件进行选择:
- Neutral文化,例如en或de。
- Specific文化,例如en-US或de-DE。
- Installed区域性,安装在REST API计算机上。
- Custom自定义用户区域性。
- Replacement已替换的默认区域性的区域性。
阅读有关.NET 区域性类型的微尘。
文化Provider
系统区域性函数在ICultureProvider接口中指定,并提供在处理API请求时限制可用区域性和控制工作区域性的能力。
public interface ICultureProvider
{
/// <summary>Get the default culture name</summary>
string DefaultCultureName { get; }
/// <summary>Get the current culture</summary>
CultureInfo CurrentCulture { get; }
/// <summary>Get the current UI culture</summary>
CultureInfo CurrentUICulture { get; }
/// <summary>Set the current application und UI culture</summary>
void SetCurrentCulture(string cultureName);
/// <summary>Get the culture by name</summary>
CultureInfo? GetCulture(string cultureName);
/// <summary>Get the supported cultures</summary>
IList<CultureInfo> GetSupportedCultures();
/// <summary>Get the supported culture descriptions</summary>
IList<CultureDescription> GetSupportedCultureDescriptions();
}
在REST应用程序中,本地化服务是在启动时设置的。
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
...
// localization
// use AddLocalizationWithRequest() to register the request localization
builder.Services.AddLocalization(
cultureScope: new(
neutral: true,
specific: true,
installed: true,
custom: false,
replacement: false),
supportedCultures: new[]
{
"en", "en-US", "en-GB",
"de", "de-DE", "de-AT", "de-CH",
"zh",
},
defaultCulture: "en-US");
var app = builder.Build();
...
}
CultureScope基本上定义了哪些系统区域性可用。这些可以用SupportedCultures进一步限制。如果事先知道数据可以本地化为哪些语言,则限制可用区域性是有意义的。对于不知道数据将被翻译成哪种语言的通用REST API,则不需要此限制。
区域性提供程序包含ASP.NET Core本地化请求本地化所需的所有信息。
区域性提供程序在DI中注册为单一实例,可在REST API控制器中用于提供来自区域性API的信息:
[ApiController]
[Route(<span class="pl-s">"cultures")</span>]
public class CulturesController : ControllerBase
{
private ICultureProvider CultureProvider { get; }
public CulturesController(ICultureProvider cultureProvider)
{
CultureProvider = cultureProvider;
}
[HttpGet(Name = <span class="pl-s">"GetCultures")</span>]
public IEnumerable<string> GetCultures() =>
CultureProvider.GetSupportedCultures()
.Select(x => x.Name).ToList();
[HttpGet(<span class="pl-s">"description", Name = "GetCultureDescriptions")</span>]
public IEnumerable<CultureDescription> GetCultureDescriptions() =>
CultureProvider.GetSupportedCultureDescriptions();
}
此示例使用GetCultures返回区域性名称列表,并使用GetCultureDescriptions端点返回英文和本机的可读描述列表。
数据本地化
本地化基于C#类属性的约定。本地化以字符串/值字典的形式存储在名为<PropertyName>Localizations的属性中。示例产品将Name属性本地化为NameLocalizations,Price属性为PriceLocalizations。
本地化扩展方法
本地化库包含多个用于对象本地化的扩展方法。
/// Test if localization property exists
bool IsLocalizable(this Type type, string propertyName);
/// Get the property localization values
Dictionary<string, object?> GetLocalizations<TObject>(
this TObject obj, string propertyName);
/// Get an optional localized property value
object? GetOptionalLocalization<TObject>(
this TObject obj, string propertyName, string? culture = null);
/// Get an optional localized property value
TValue? GetOptionalLocalization<TObject, TValue>(
this TObject obj, string propertyName, string? culture = null);
/// Get the localized property value
TValue GetLocalization<TObject, TValue>(
this TObject obj, string propertyName, string? culture = null);
/// Map all source object localized values to the target object base properties
TTarget MapLocalizations<TTarget, TSource>(
this TTarget target, TSource source, string? culture = null);
/// Map a source object localized value to the target object base property
void MapLocalization<TTarget, TSource>(
this TTarget target, TSource source, string propertyName, string? culture = null)
Product本地化
以下产品可用于水果在线商店。
[
{
"name": "Nectarine",
"nameLocalizations": {
"en": "Nectarine",
"de": "Nektarine",
"zh-CN": "油桃"
},
"price": 0,
"priceLocalizations": {
"en": 3,
"de": 3.6,
"de-CH": 3.9,
"zh-CN": 2.8
}
},
{
"name": "GoldenMelon",
"nameLocalizations": {
"en": "Golden Melon",
"de": "Honigmelone",
"zh-CN": "金瓜"
},
"price": 0,
"priceLocalizations": {
"en": 4.5,
"de": 5.7,
"de-CH": 6.2,
"zh-CN": 4
}
}
]
该产品由以下DTO描述:
public class ProductDto
{
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
在产品控制器中,GetProducts终结点返回可本地化的产品,该GetProductsDto方法返回存储的DTO。
[ApiController]
[Route(<span class="pl-s">"products")</span>]
public class ProductsController : ControllerBase
{
[HttpGet(Name = <span class="pl-s">"GetProducts")</span>]
public IEnumerable<Product> GetProducts()
{
var products = new ProductService().GetProducts();
return products;
}
[HttpGet(<span class="pl-s">"dto", Name = "GetProductsDto")</span>]
public IEnumerable<ProductDto> GetProductsDto(
[FromQuery] string? culture = null)
{
// map products to dto objects
var config = new MapperConfiguration(
cfg => cfg.CreateMap<Product, ProductDto>());
var mapper = new Mapper(config);
var products = new ProductService().GetProducts();
var dataProducts = products.ConvertAll(
// map object
x => mapper.Map<ProductDto>(x)
// map localizations
.MapLocalizations(x, culture)).ToList();
return dataProducts;
}
}
若要将产品转换为DTO,首先使用 AutoMapper mapper.Map<ProductDto>映射对象,然后MapLocalizations()用于将本地化应用于DTO。
此示例的REST API可以使用Visual Studio解决方案ObjectLocalization.WebApi.sln启动。
为了简化示例,产品存储在本地JSON文件中。
Blazor客户端应用程序
为了说明客户端中的本地化,有一个使用开源 MudBlazor UI框架的Blazor应用程序。
应用程序在Cultures页面上列出了REST API的可用语言。
该Products页面列出了产品,包括所有翻译,以及列出的DTO。
DTO产品会随着Culture变化而相应地进行调整。
本地化数据的关系持久性
可以通过多种方式在关系数据库中实现本地化数据:
- Table:本地化在单独的表中维护。
- Column:本地化在附加列中以JSON形式进行管理。
哪种变体有意义取决于几个因素
- 本地化是否可索引(性能)->表
- 本地化是否可寻址(模型引用)->表
- 本地化是可搜索的(REST请求)-> 表(简单)还是列(复杂)
- 本地化对象是否保持紧凑(审核)-> 列
- 数据模型是否应尽可能简单->列
本地化表
此变体为每个可本地化的字段创建一个本地化表。
本地化列
当本地化数据存储在附加列中时,它将序列化为 JSON。
大多数ORM工具都提供了在字段中序列化字典的功能。
Entity Framework代码优先本地化
将NotMapped属性与包含序列化JSON的其他字符串字段一起使用。
using System.Text.Json;
public class Product
{
public string Name { get; set; }
[NotMapped]
public Dictionary<string, string> NameLocalizations { get; set; }
public string NameLocalizationsJson
{
get => JsonSerializer.Serialize<Dictionary<string, string>>(NameLocalizations);
set => NameLocalizations = JsonSerializer.Deserialize<Dictionary<string, string>(value);
}
public decimal Price { get; set; }
[NotMapped]
public Dictionary<string, decimal> PriceLocalizations { get; set; }
public decimal PriceLocalizationsJson
{
get => JsonSerializer.Serialize<Dictionary<string, decimal>>(PriceLocalizations);
set => PriceLocalizations = JsonSerializer.Deserialize<Dictionary<string, decimal>>(value);
}
}
Dapper本地化
使用Dapper,您可以使用自定义类型处理程序转换数据。
using System.Text.Json;
public class NamedDictionaryTypeHandler<TValue> :
SqlMapper.TypeHandler<Dictionary<string, TValue>>
{
public override void SetValue(IDbDataParameter parameter,
Dictionary<string, TValue> value)
{
parameter.Value = JsonSerializer.Serialize<Dictionary<string, TValue>>(value);
}
public override Dictionary<string, TValue> Parse(object value)
{
var json = value as string;
if (string.IsNullOrWhiteSpace(json))
{
return null;
}
return JsonSerializer.Deserialize<Dictionary<string, TValue>>(json);
}
}
类型处理程序必须在程序开始时以SqlMapper.AddTypeHandler(new NamedDictionaryTypeHandler<object>());注册。
本文最初发表于 GitHub - Giannoudis/RestApiLocalization: REST API Localization for .NET
https://www.codeproject.com/Articles/5367347/Data-driven-Localization-for-NET-REST-APIs