2

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?

  • 1
    It sounds like you need many DTOs, one for each language. Using collection (e.g. `Dictionary`) instead of fixed class should do. – Sinatr Dec 10 '20 at 11:23
  • 2
    You should excuse me, but you really should explain the reason behind _"I want to localize (translate) my DTO's properties"_. What is the purpose of localizing the properties names of your DTO?. This should be a problem to be solved at the point where you display a label attached to the class information not creating different classes for different languages. – Steve Dec 10 '20 at 11:26
  • @Steve Thanks for your answer. This is what a frontend developer needs and told me to implement. Just assume we want to fill a table, so values are integers (in my case) but these properties should be localized, because not everyone in my country knows english. I said before I'm not sure this is a common task to do. Thanks any way. – abalfazl9776 Dec 10 '20 at 11:36
  • @Sinatr Oh GOD please no! – abalfazl9776 Dec 10 '20 at 11:38
  • 2
    @abalfazl9776 tell your frontend developper if they are not able to localize a set of labels they are not really good at their work. I repeat, creating different classes with different properties names will result in an infinite series of bugs and colossal inefficiencies. Just because they don't want to use something to localize those prop names when they display the values? – Steve Dec 10 '20 at 12:35
  • Thanks, I will.@Steve – abalfazl9776 Dec 10 '20 at 13:18
  • I'm going to agree with Steve. Localizing the json files sounds like a complete nightmare. It might be better to ask how you should do localization for your particular type of application. – JonasH Dec 10 '20 at 15:31
  • 1
    Agree with everyone else that localizing property names is a **Really Bad Idea**. The reason you are seeing the property names translated as per the first request is that [`DefaultContractResolver` caches type information](https://stackoverflow.com/a/33558665/3744182). As that is causing problems for you, you could manually serialize to JSON using a new contract resolver each time, and return the JSON literal as shown in e.g. [this answer](https://stackoverflow.com/a/64764620/3744182) to [Return a JSON string explicitly from Asp.net WEBAPI?](https://stackoverflow.com/q/17097841/3744182). – dbc Dec 16 '20 at 22:18

0 Answers0