LazZiya / XLocalizer

Localizer package for Asp.Net Core web applications, powered by online translation and auto resource creating.
https://docs.ziyad.info
129 stars 14 forks source link

ExDataAnnotations with a DefaultRequestCulture different from "en" #2

Closed Gaudico closed 4 years ago

Gaudico commented 4 years ago

Hi! First of all, I want to tell you that what you are doing here is a hell of a job and I love it.

Well, to the point: I have seen that if the "DefaultRequestCulture" is different from "en", the texts of the "ExDataAnnotations" are not entered in the default language file, so the default English text is used. If we then enter this text in the xml by hand, it works correctly.

My suggestion is that, if the texts that come from the Framewok can be identified in some way and the "DefaultRequestCulture" is different from "en", they should always be entered and translated in their xml file, and if we cannot identify them, then all the texts should be copied (but not translated), so it will be easier to locate these texts to do it manually.

However, the latter should always be a configurable option, so that if it is active, all texts are always available to be changed without having to redeploy the application again if the default language texts have to be changed.

What do you think about all this?

LazZiya commented 4 years ago

Hi @Gaudico and many thanks for rising this issue and your suggestions as well.

For data annotations, if you provide the error message inside the attribute tag it should work:

[Required(ErrorMessage = "...error message in xx culture...")]

// or

[ExRequired(ErrorMessage = "...error message in xx culture...")]

The default request culture "xx" must be the same in RequestLocalizationOptions in startup configurations.


But we still have the problem with identity and model binding errors! So, I can think of some options like below:

A- Providing a custom interface to override the default system messages for the default culture. This way, all messages can be overridden easily and configured in startup with XLocalizer.

B- You already mentioned, the system messages can be translated always from "en" culture, then can be identified and amended manually in the resource file. But in this case, if the user has already provided a localized message in other culture it will not work, because the provided message will override the default message.

C- One more possible solution, I can check the default culture, if it is different than "en" then first translate the "en" messages to the users default culture, then provide the message. But this also adds another layer of complexity.

I think option A looks the most efficient way, I will do some more checks to see the best solution :)

Gaudico commented 4 years ago

Option A seems to be the most reasonable.

Regarding the option that the texts are always inserted in the default language file (without being modified, only the text as it is), what do you think? Can you enter it as an option?

LazZiya commented 4 years ago

I've introduced two new interfaces that allows to override the default error messages of model binding and identity errors.

How to use:

Example

Overriding of model binding errors for the default culture "ar"

public class CustomModelBindingErrors : IModelBindingErrorMessagesProvider
{
    string IModelBindingErrorMessagesProvider.AttemptedValueIsInvalidAccessor => "هذه القيمة غير مقبولة من {0} الى {1}، غير لو سمحت";

    string IModelBindingErrorMessagesProvider.MissingBindRequiredValueAccessor => "هذه القيمة غير مقبولة من {0} غير لو سمحت";

    string IModelBindingErrorMessagesProvider.MissingKeyOrValueAccessor => "هذه القيمة غير مقبولة غير لو سمحت";

    string IModelBindingErrorMessagesProvider.MissingRequestBodyRequiredValueAccessor => "هذه القيمة غير مقبولة من غير لو سمحت";

    string IModelBindingErrorMessagesProvider.NonPropertyAttemptedValueIsInvalidAccessor => "هذه القيمة غير مقبولة من {0} غير لو سمحت";

    string IModelBindingErrorMessagesProvider.NonPropertyUnknownValueIsInvalidAccessor => "هذه القيمة غير مقبولة من غير لو سمحت";

    string IModelBindingErrorMessagesProvider.NonPropertyValueMustBeANumberAccessor => "هذه القيمة غير مقبولة من غير لو سمحت";

    string IModelBindingErrorMessagesProvider.UnknownValueIsInvalidAccessor => "هذه القيمة غير مقبولة من {0} غير لو سمحت";

    string IModelBindingErrorMessagesProvider.ValueIsInvalidAccessor => "هذه القيمة غير مقبولة من {0} غير لو سمحت";

    string IModelBindingErrorMessagesProvider.ValueMustBeANumberAccessor => "هذه القيمة غير مقبولة من {0} غير لو سمحت";

    string IModelBindingErrorMessagesProvider.ValueMustNotBeNullAccessor => "هذه القيمة غير مقبولة من {0} غير لو سمحت";
}

Register in startup:

services.AddSingleton<IModelBindingErrorMessagesProvider, CustomModelBindingErrors>();

These errors will override the system default errors for the default culture, so if you have a default culture other that "en" you can provide your localized errors in this class. This way XLocalizer can translate and add them to the resource file.

Same procudure can be done for identity errors as well by implementing IIdentityErrorMessagesProvider.

Last but not least, for data annotations, the only possible way is to provide the custom error message inside the attribute tag:

[Required(ErrorMessage = "..my custom error in xx culture .. ")]
LazZiya commented 4 years ago

Re insertng the text in the default language, you can do so by switching off the translation and keep AutoAddKeys on:

services.AddRazorPages()
    .AddXLocalizer<LocSource>(ops =>
    {
        ops.ResourcesPath = "LocalizationResources";
        ops.AutoAddKeys = true;
        ops.AutoTranslate = false;
    });

The downside of this approach that it will switch off translation for all localization. May be in a feature release I can add more customization to switch on/off translatio for specific parts of the app :)

LazZiya commented 4 years ago

Changes are available in preview3 and available to download from nuget.

I will close this issue, since I think a solution has been provided. Please feel free to keep the discussion up if you still have quesitons.

Gaudico commented 4 years ago

Re insertng the text in the default language, you can do so by switching off the translation and keep AutoAddKeys on:

services.AddRazorPages()
    .AddXLocalizer<LocSource>(ops =>
    {
        ops.ResourcesPath = "LocalizationResources";
        ops.AutoAddKeys = true;
        ops.AutoTranslate = false;
    });

The downside of this approach that it will switch off translation for all localization. May be in a feature release I can add more customization to switch on/off translatio for specific parts of the app :)

Sorry but with the last version switching off the translation and keep AutoAddKeys is not working for me, the file is generated but without any resources in it.

LazZiya commented 4 years ago

Can you post a sample setup of your project? I tried and it worked fine, I couldn't reproduce your issue.

Gaudico commented 4 years ago

I have created a new clean asp.net core 3.1 project with MVC and added my XLocalizer configuration to it and it still doesn't work for me with preview3. And I found another error if we add the Convention RouteTemplateModelConventionMvc causes it not to know how to find the route.

Startup.cs:

public class Startup
{
    public Startup(IConfiguration configuration, IWebHostEnvironment env)
    {
        Configuration = configuration;
        Env = env;
    }

    public IConfiguration Configuration { get; }

    private IWebHostEnvironment Env { get; }

    private static readonly CultureInfo[] SupportedCultures = {
        new CultureInfo("es-ES"),
        new CultureInfo("en-US"),
        new CultureInfo("pl-PL"),
    };

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        // Configure request localization options
        services.Configure<RequestLocalizationOptions>(ops =>
        {
            // Define supported cultures
            ops.SupportedCultures = SupportedCultures;
            ops.SupportedUICultures = SupportedCultures;
            ops.DefaultRequestCulture = new RequestCulture(SupportedCultures.First());

            // Optional: add custom provider to support localization based on {culture} route value
            ops.RequestCultureProviders.Insert(0, new RouteSegmentRequestCultureProvider(SupportedCultures));
        });
        // Optional: To enable online translation register one or more translation services
        services.AddSingleton<IXResourceProvider, XmlResourceProvider>();

        // Comment below line to switch to RESX based localization.
        services.AddHttpClient<ITranslator, MyMemoryTranslateService>();

        services.AddMvc()
            //.AddMvcOptions(ops => { ops.Conventions.Insert(0, new RouteTemplateModelConventionMvc()); })// This option causes it not to know how to find the route
            .AddXLocalizer<LocSource, MyMemoryTranslateService>(ops =>
            {
                ops.ResourcesPath = "LocalizationResources";
                ops.UseExpressValidationAttributes = false;
                ops.UseExpressMemoryCache = !Env.IsDevelopment();

                ops.AutoAddKeys = true;
                ops.AutoTranslate = false;
            });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRequestLocalization();

        app.UseRouting();

        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute(
                name: "default",
                pattern: "{culture=es-ES}/{controller=Home}/{action=Index}/{id?}");
        });
    }
}

_ViewImports.cshtml:

@using WebApplication1
@using WebApplication1.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, XLocalizer.TagHelpers
@inject Microsoft.Extensions.Localization.IStringLocalizer  Loc

Index.cshtml:

@{
    ViewData["Title"] = Loc["Home Page"];
}
<div class="text-center">
    <h1 class="display-4" localize-content>Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

Generated LocSource.es-Es.xml:

<?xml version="1.0" encoding="utf-8" ?>
<root>
    <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
        <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
        <xsd:element name="root">
            <xsd:complexType>
                <xsd:sequence>
                    <xsd:element name="data" maxOccurs="unbounded" minOccurs="0">
                        <xsd:complexType>
                            <xsd:sequence>
                                <xsd:element name="key">
                                    <xsd:complexType>
                                        <xsd:simpleContent>
                                            <xsd:extension base="xsd:string">
                                                <xsd:attribute type="xsd:string" name="isActive" use="optional"/>
                                            </xsd:extension>
                                        </xsd:simpleContent>
                                    </xsd:complexType>
                                </xsd:element>
                                <xsd:element type="xsd:string" name="value"/>
                                <xsd:element type="xsd:string" name="comment"/>
                            </xsd:sequence>
                            <xsd:attribute type="xsd:string" name="isActive" use="optional"/>
                        </xsd:complexType>
                    </xsd:element>
                </xsd:sequence>
            </xsd:complexType>
        </xsd:element>
    </xsd:schema>
</root>
LazZiya commented 4 years ago

Well, I think I have to work more on the documentaions :) Let me explain the translation behavior of XLocalizer for different cultures;

The translation is done from the default culture to the selected culture. So, if our default culture is "es" and the the selected culture is "en" the translation will be from "es" to "en" and XLocalizer will create a resource file for the selected culture "LocSource.en.xml".

Translation behavior of XLocalizer: From "Default Culture" --> To "Selected Culture".

Back to your case, if we have these cultures { "en", "es", "pl" }, and the default culture is "es", so the source culture for translation will always be "es".

XLocalizer assumes that the default texts in the app are written in the default culture

So, in other words; if "default culture" == "selected culture" no translation, no resources.

A possible solution can be done by adding a new parameter to XLocalizerOptions to define the source culture for translation, but I don't want to overload the options with too many culture params to not create confusion of what is what..., thats why I went with the default culture.


Re the other problem related to MVC route, let's open a new issue to follow it, for now you can avoid it by commenting the line related to RouteTemplateModelConventionMvc and use the culture route template in the route table as ou already did.

LazZiya commented 4 years ago

Hi @Gaudico , just re-evaluated the translation culture issue, and I've added a new option to XLocalizerOptions to specifiy the translation source culture, if not specified the default culture will be used instead.

services.AddRazorPages()
    .AddXLocalizer<LocSource, TranslationServiceName>(ops =>
    {
        // ...
        ops.AutoTranslate = true;
        ops.TranslateFromCulture = "es"
    });

This feature will be available with the next preview release,

Hope this will help :)

B. Regards, Ziya

LazZiya commented 4 years ago

@Gaudico , I checked the routing issue, all you have to do is to add a route attribute to your actions, if two or more actions has no route attribute, it will throw an exception due to similar routes.

public IActionResult Index()
{
    // ...
}

[Route("privacy")]
public IActionResult Privacy()
{
    // ...
}

[Route("error")]
public IActionResult Error()
{
    // ...
}
LazZiya commented 3 years ago

@Gaudico Hi again, just want to mention that with the first release things changed a bit, it is now easier to override all data annotations errors, model binding errors and identity errors in one place. See below docs for more details:

Malkawi1 commented 1 year ago

hi sir,

how to edit this code -> // add culture route segment for controllers e.g. /en/Home/Index .AddMvcOptions(ops => ops.Conventions.Insert(0, new RouteTemplateModelConventionMvc()))

because make this error An unhandled exception occurred while processing the request. AmbiguousMatchException: The request matched multiple endpoints. Matches:

Microsoft.AspNetCore.OData.Routing.Controllers.MetadataController.GetServiceDocument (Microsoft.AspNetCore.OData) Microsoft.AspNetCore.OData.Routing.Controllers.MetadataController.GetMetadata (Microsoft.AspNetCore.OData) Link.Controllers.GroupsController.ConfirmEmail (Link) Link.Controllers.HomeController.Error (Link) Link.Controllers.HomeController.ChatBot (Link) Link.Controllers.HomeController.Index (Link) Link.Controllers.GroupsController.SendRequest (Link) Link.Controllers.CommentsController.Index (Link) Link.Controllers.GroupsController.Create (Link) Link.Controllers.GroupsController.Details (Link) Link.Controllers.GroupsController.Groups (Link) Link.Controllers.GroupsController.Index (Link) Link.Controllers.CommentsController.Hashtag (Link) Link.Areas.Admin.Controllers.HomeController.Index (Link) Link.Controllers.GroupsController.Edit (Link) Link.Controllers.PeopleController.Index (Link) Microsoft.AspNetCore.Routing.Matching.DefaultEndpointSelector.ReportAmbiguity(CandidateState[] candidateState)

LazZiya commented 1 year ago

@Malkawi1 I moved your comment to a new issue #38