I'm working on an asp.net core 5 web api project and I want to localize (translate) my DTO's properties when serializing and converting them to json. For example assume we have this class:
public class PersonDto
{
public string Name { get; set; }
public int Grade { get; set; }
}
So when I try to serialize this object JsonConvert.Serialize(personDto)
I got a json like:
{
"name": "any name",
"grade": 90
}
But when a user sends Accept-Language: fr-FR
header I want to return a response like:
{
"nom": "any name",
"classe": 90
}
(I don't know french I just used google translate to demonstrate my purpose.)
What I have done until now:
First I added localization to my project. Here is the configuration:
services.AddLocalization();
services.Configure<RequestLocalizationOptions>(
options =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR")
};
options.DefaultRequestCulture = new RequestCulture(culture: "fr-FR", uiCulture: "fr-FR");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
options.RequestCultureProviders = new[] { new AcceptLanguageHeaderRequestCultureProvider() };
});
And for Configure part of startup:
// ..
app.UseRouting();
app.UseRequestLocalization();
// ..
And localization works in a TestContoller I just wrote:
public class Test : BaseController // BaseController is inherited from ControllerBase (it's OK)
{
private readonly IStringLocalizer<Resource> _localizer;
public Test(IStringLocalizer<Resource> localizer)
{
_localizer = localizer;
}
[HttpGet]
public string Get()
{
return _localizer.GetString("Hello");
}
}
So when we send Accept-Language: fr-FR
header we get Bonjour as response and it's OK.
But I don't know how to translate class properties. I tried to write a CustomContractResolver and it worked but it has a bug. Here is the ContractResolver:
public class CustomContractResolver : DefaultContractResolver
{
private readonly HashSet<Type> _localizeds;
private readonly IStringLocalizer<Resource> _localizer;
public CustomContractResolver(IStringLocalizer<Resource> localizer)
{
_localizer = localizer;
_localizeds = new HashSet<Type>();
}
public void Localize(Type type)
{
if (!_localizeds.Contains(type))
_localizeds.Add(type);
}
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
{
var property = base.CreateProperty(member, memberSerialization);
if (IsLocalized(property.DeclaringType, property.PropertyName))
{
property.PropertyName = _localizer.GetString(property.PropertyName);
}
return property;
}
private bool IsLocalized(Type type, string jsonPropertyName)
{
if (!_localizeds.Contains(type))
return false;
return type.GetProperties().Any(p => p.Name.Equals(jsonPropertyName));
}
}
And I added this to JsonDefaultSettings
by calling this app.ChangePropertyNameForLocalizableClasses();
in Configure method of startup:
public static class NewtonsoftJsonOptionsExtensions
{
public static void ChangePropertyNameForLocalizableClasses(this IApplicationBuilder app)
{
// CustomLocalizeAttribute is a custom attribute that I add to classes that I want to localize them when serializng
// We are using reflection to get all the classes that should be localized when serializing
var types = Assembly.GetExecutingAssembly().GetTypes()
.Where(c => c.HasAttribute<CustomLocalizeAttribute>() && !c.IsAbstract && c.IsPublic && c.IsClass);
// Our problem starts here: I have to inject localizer manually and DI isn't working
var localizer = app.ApplicationServices.GetRequiredService<IStringLocalizer<Resource>>();
var jsonResolver = new PropertyRenameAndIgnoreSerializerContractResolver(localizer);
foreach (var type in types)
{
jsonResolver.Localize(type);
}
JsonConvert.DefaultSettings = () =>
{
var settings = new JsonSerializerSettings {ContractResolver = jsonResolver};
return settings;
};
}
}
So my problem is when the program starts, if the first request have Accept-Language: fr-FR
header then all classes that have CustomLocalizeAttribute
will be localized when serializing and they ignore what Accept-Language
header actually is.
How can I get a new instance of _localizer
(IStringLocalizer) with every request?
Maybe I'm going all the way wrong! Is this a common task to do? Is there any other solutions?