How to localize class properties when serializing?

Viewed 365

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?

0 Answers
Related