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

fail: XLocalizer.Resx.ResxResourceProvider[0] #35

Closed EasyLOB closed 2 years ago

EasyLOB commented 2 years ago

Hi,

First, congratulations for XLocalizer :smiley: It's really a nice Library and finally I found a solution to translate .NET 6 Data Annotation errors. Almost unbelievable Data Annotations translation is not available in .NET 6 as it was in .NET Framework.

I created a sample project and it works fine ( ASP.NET does not show any error ), BUT, watching Kestrel messages the following "fail" is shown:

fail: XLocalizer.Resx.ResxResourceProvider[0]
Could not find the resource "Northwind.Mvc.XLocalizer.XLocalizerSource.resources"
among the resources "" embedded in the assembly "Northwind.Mvc",
nor among the resources in any satellite assemblies for the specified culture.
Perhaps the resources were embedded with an incorrect name.

I will not use Resource Files, but trying to remove this "fail" I created one: but the "fail" is still there. I will not use Translation too: I just translated by myself the Data Annotation errors to pt-BR. Below you find the current configuration and the files I created. Both defining in JSON ( appsettings.json ) or defining in CODE ( Startup.cs ) works: the only problem is the "fail" message. What am I doing wrong ?

Thanks

\XLocalizer
    XLocalizerSource.cs ( namespace Northwind.Mvc.XLocalizer )
    XLocalizerSource.en-US.cs
    XLocalizerSource.pt-BR.cs  

Startup.cs

IMvcBuilder mvcBuilder = services
    .AddControllersWithViews(o => o.EnableEndpointRouting = false)
    .AddNewtonsoftJson(o =>
    {
        o.SerializerSettings.ContractResolver = new DefaultContractResolver();
        o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
    })
    // IStringLocalizer
    //.AddDataAnnotationsLocalization()
    //.AddDataAnnotationsLocalization(o =>
    //{
    //    o.DataAnnotationLocalizerProvider = (type, factory) =>
    //        o.Create(typeof(SharedResource));
    //})
    //.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
    .AddViewOptions(o =>
    {
        o.HtmlHelperOptions.ClientValidationEnabled = true;
    })
    // XLocalizer
    // JSON
    //.AddXLocalizer<XLocalizerSource>(o => configuration.GetSection("XLocalizerOptions").Bind(o));
    // C#
    .AddXLocalizer<XLocalizerSource>(o =>
    {
        o.AutoAddKeys = false;
        o.AutoTranslate = false;
        o.ResourcesPath = "XLocalizer";
        o.TranslateFromCulture = "pt-BR";
        o.UseExpressMemoryCache = true;
        o.IdentityErrors = new IdentityErrors 
        {
            DuplicateEmail = "O e-mail '{0}' já foi levado.",
LazZiya commented 2 years ago

Hi @EasyLOB ,

Glad to know this library is helping you :)

Can you share your RequestLocalizationOptions configuration settings in startup? what is the default culture there?

EasyLOB commented 2 years ago

Hi,

I was not using RequestLocalizationOptions. I have an Open Source Framework ( EasyLOB ), currently migrating to .NET 6. I still use the "old" .NET Framework .resx Resources with public classes model to keep compatibility with .NET Framework EasyLOB 3.

I included it ( see below ) but the fail is still there. Why is XLocalizer looking for Resources when I am not using any one ?

using EasyLOB.Identity;
using FastReport.Utils;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using Northwind.Mvc.XLocalizer;
using System;
using System.Globalization;
using System.Threading;
using XLocalizer;
using XLocalizer.ErrorMessages;

namespace EasyLOB.Mvc
{
    public static class Startup // ###
    {
        public static void ConfigureServices(IServiceCollection services,
            IConfiguration configuration)
        {
            // Globalization
            // XLocalizer ###
            services.AddLocalization(o => o.ResourcesPath = "Resources");
            services.Configure<RequestLocalizationOptions>(o =>
            {
                var supportedCultures = new[]
                {
                    new CultureInfo("en-US"),
                    new CultureInfo("pt-BR")
                };
                o.SupportedCultures = supportedCultures;
                o.SupportedUICultures = supportedCultures;
                o.DefaultRequestCulture = new RequestCulture("pt-BR");
            });

            // EnvironmentManagerWeb
            services.AddHttpContextAccessor();

            // ASP.NET Core MVC
            IMvcBuilder mvcBuilder = services
                .AddControllersWithViews(o => o.EnableEndpointRouting = false)
                // Microsoft.AspNetCore.Mvc.NewtonsoftJson
                .AddNewtonsoftJson(o =>
                {
                    o.SerializerSettings.ContractResolver = new DefaultContractResolver();
                    o.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
                })
                // IStringLocalizer
                //.AddDataAnnotationsLocalization()
                //.AddDataAnnotationsLocalization(o =>
                //{
                //    o.DataAnnotationLocalizerProvider = (type, factory) =>
                //        o.Create(typeof(SharedResource));
                //})
                //.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
                .AddViewOptions(o =>
                {
                    o.HtmlHelperOptions.ClientValidationEnabled = true;
                })
                // XLocalizer
                // JSON
                //.AddXLocalizer<XLocalizerSource>(o => configuration.GetSection("XLocalizerOptions").Bind(o));
                // C#
                .AddXLocalizer<XLocalizerSource>(o =>
                {
                    o.AutoAddKeys = false;
                    o.AutoTranslate = false;
                    o.ResourcesPath = "XLocalizer";
                    o.TranslateFromCulture = "pt-BR";
                    o.UseExpressMemoryCache = true;
                    o.IdentityErrors = new IdentityErrors 
                    {
                        DuplicateEmail = "O e-mail '{0}' já foi levado.",
                        DuplicateUserName = "O nome do usuário '{0}' já está tomado. Por favor, tente outro.",
                        InvalidEmail = "O e-mail '{0}' é inválido.",
                        DuplicateRoleName = "O nome da função '{0}' já está tomado.",
                        InvalidRoleName = "O nome da função '{0}' é inválido.",
                        InvalidToken = "Token inválido.",
                        InvalidUserName = "O nome de usuário '{0}' é inválido, só pode conter letras ou dígitos.",
                        LoginAlreadyAssociated = "Um usuário com esse login já existe.",
                        PasswordMismatch = "Senha incorreta.",
                        PasswordRequiresDigit = "As senhas devem ter pelo menos um dígito ('0'-'9').",
                        PasswordRequiresLower = "As senhas devem ter pelo menos uma minúscula ('a'-'z').",
                        PasswordRequiresNonAlphanumeric = "As senhas devem ter pelo menos um caractere não alfanumérico.",
                        PasswordRequiresUniqueChars = "As senhas devem ser usada pelo menos {0} caracteres diferentes.",
                        PasswordRequiresUpper = "As senhas devem ter pelo menos uma maiúscula ('A'-'Z').",
                        PasswordTooShort = "As senhas devem ser pelo menos {0} caracteres.",
                        UserAlreadyHasPassword = "O usuário já tem um conjunto de senhas.",
                        UserAlreadyInRole = "Usuário já na função '{0}'.",
                        UserNotInRole = "O usuário não está na função '{0}'.",
                        UserLockoutNotEnabled = "O bloqueio não está habilitado para este usuário.",
                        RecoveryCodeRedemptionFailed = "O resgate do código de recuperação falhou.",
                        ConcurrencyFailure = "Falha de concorrência otimista, o objeto foi modificado.",
                        DefaultError = "Uma falha desconhecido ocorreu."
                    };
                    o.ModelBindingErrors = new ModelBindingErrors
                    {
                        AttemptedValueIsInvalidAccessor = "O valor '{0}' não é válido para {1}.",
                        MissingBindRequiredValueAccessor = "Não foi fornecido um valor para o parâmetro ou propriedade '{0}'.",
                        MissingKeyOrValueAccessor = "Um valor é necessário.",
                        MissingRequestBodyRequiredValueAccessor = "É necessário um corpo de solicitação não vazio.",
                        NonPropertyAttemptedValueIsInvalidAccessor = "O valor '{0}' não é válido.",
                        NonPropertyUnknownValueIsInvalidAccessor = "O valor fornecido é inválido.",
                        NonPropertyValueMustBeANumberAccessor = "O campo deve ser um número.",
                        UnknownValueIsInvalidAccessor = "O valor fornecido é inválido para {0}.",
                        ValueIsInvalidAccessor = "O valor '{0}' é inválido.",
                        ValueMustBeANumberAccessor = "O campo {0} deve ser um número. Não use letras ou caracteres especiais.",
                        ValueMustNotBeNullAccessor = "O valor '{0}' é inválido. Isso não pode ser nulo."
                    };
                    o.ValidationErrors = new ValidationErrors 
                    {
                        CompareAttribute_MustMatch = "'{0}' e '{1}' não combinam. Eles não deveriam ser diferentes!",
                        CreditCardAttribute_Invalid = "O campo {0} não é um número de cartão de crédito válido.",
                        CustomValidationAttribute_ValidationError = "{0} não é válido.",
                        DataTypeAttribute_EmptyDataTypeString = "A sequência de DataType personalizada não pode ser nula ou vazia.",
                        EmailAddressAttribute_Invalid = "O campo {0} não é um endereço de e-mail válido.",
                        FileExtensionsAttribute_Invalid = "O campo {0} só aceita arquivos com as seguintes extensões: {1}",
                        MaxLengthAttribute_ValidationError = "O campo {0} deve ser um tipo de string ou array com um comprimento máximo de '{1}'.",
                        MinLengthAttribute_ValidationError = "O campo {0} deve ser um tipo de string ou array com um comprimento mínimo de '{1}'.",
                        PhoneAttribute_Invalid = "O campo {0} não é um número de telefone válido.",
                        RangeAttribute_ValidationError = "O {0} de campo deve ser entre {1} e {2}.",
                        RegexAttribute_ValidationError = "O campo {0} deve corresponder à expressão regular \"{1}\".",
                        RequiredAttribute_ValidationError = "O campo {0} é obrigatório.",
                        StringLengthAttribute_ValidationError = "O campo {0} deve ser uma sequência com um comprimento máximo de {1}.",
                        StringLengthAttribute_ValidationErrorIncludingMinimum = "O campo {0} deve ser uma sequência com um comprimento mínimo de {2} e um comprimento máximo de {1}.",
                        UrlAttribute_Invalid = "O campo {0} não é um URL http, https ou ftp totalmente qualificado.",
                        ValidationAttribute_ValidationError = "O campo {0} é inválido."
                    };
                });

            //#if DEBUG

            //<PropertyGroup>
            //  <RazorCompileOnBuild>true</RazorCompileOnBuild>
            //  <RazorCompileOnPublish>true</RazorCompileOnPublish>
            //</PropertyGroup>

            // Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation
            mvcBuilder.AddRazorRuntimeCompilation();

            //#endif

            // Kestrel
            services.Configure<KestrelServerOptions>(configuration.GetSection("Kestrel"));

            // Razor
            services.Configure<RazorViewEngineOptions>(o => {
                o.ViewLocationExpanders.Add(new CustomViewLocationExpander());
            });

            // Session
            //services.AddCaching();
            services.AddSession(o => {
                o.IdleTimeout = TimeSpan.FromMinutes(60);
            });

            // Entity Framework
            //StartupHelper.EntityFramework(services, ZDBMS.MySQL);
            //StartupHelper.EntityFramework(services, ZDBMS.PostgreSQL);
            StartupHelper.EntityFramework(services, ZDBMS.SQLServer);

            // ASP.NET Core Identity
            //StartupHelper.EntityFrameworkIdentity(services, ZDBMS.MySQL);
            //StartupHelper.EntityFrameworkIdentity(services, ZDBMS.PostgreSQL);
            StartupHelper.EntityFrameworkIdentity(services, ZDBMS.SQLServer);
            services
                .AddIdentity<AppUser, AppRole>(o => {
                    o.Password.RequiredLength = 6;
                    o.Password.RequireDigit = true;
                    o.Password.RequireLowercase = true;
                    o.Password.RequireNonAlphanumeric = true;
                    o.Password.RequireUppercase = true;
                    o.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyz0123456789_";
                    o.User.RequireUniqueEmail = true;
                })
                .AddEntityFrameworkStores<AppIdentityDbContext>()
                .AddDefaultTokenProviders();
            services.ConfigureApplicationCookie(o => o.LoginPath = "/Security/Login");
            //services.AddTransient<IPasswordValidator<AppUser>, CustomPasswordValidator>();
            //services.AddTransient<IRoleValidator<RoleUser>, CustomRoleValidator>();
            //services.AddTransient<IUserValidator<AppUser>, CustomUserValidator>();
        }

        // ConfigureServices -> Configure
        public static void Configure(IApplicationBuilder applicationBuilder,
            IWebHostEnvironment environment)
        {
            //ILifetimeScope container = applicationBuilder.ApplicationServices.GetAutofacRoot();

            // Culture
            //applicationBuilder.UseRequestLocalization(); // ###
            applicationBuilder.Use(async (context, next) =>
            {
                // ###

                // .AspNetCore.Culture : c=en-US|uic=en-US
                //string culture = context.Request.Cookies[CookieRequestCultureProvider.DefaultCookieName];
                //if (culture == null)
                //{
                //    context.Response.Cookies.Append(CookieRequestCultureProvider.DefaultCookieName,
                //        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("en-US")),
                //        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) });
                //}

                string cookie = context.Request.Cookies["ZCulture"];
                if (cookie == null)
                {
                    cookie = "en-US";
                    context.Response.Cookies.Append("ZCulture",
                        cookie,
                        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) });
                }

                CultureInfo ci = new CultureInfo(cookie); // en-US | en

                Thread.CurrentThread.CurrentCulture = ci;
                Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(ci.Name);

                CultureInfo.DefaultThreadCurrentCulture = ci;
                CultureInfo.DefaultThreadCurrentUICulture = ci;

                await next.Invoke();
            });

            // ASP.NET Core            
            //if (environment.IsDevelopment())
            {
                applicationBuilder.UseDeveloperExceptionPage();
            }
            applicationBuilder.UseRouting();
            applicationBuilder.UseSession();
            applicationBuilder.UseStaticFiles();
            applicationBuilder.UseStatusCodePages();

            // ASP.NET Core Identity
            applicationBuilder.UseAuthentication();
            applicationBuilder.UseAuthorization();

            // ASP.NET Core MVC
            applicationBuilder.UseMvc(routes =>
            {
                routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
            });

            // FastReport
            RegisteredObjects.AddConnection(typeof(MsSqlDataConnection));
            applicationBuilder.UseFastReport();
        }
    }
}
EasyLOB commented 2 years ago

Hi, I will probably find the above code "strange", but as I may use Autofac or .NET DI, I structure my code this way:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Syncfusion.Licensing;

namespace EasyLOB.Mvc
{
    public class StartupCore
    {
        public IConfiguration Configuration { get; }

        public StartupCore(IConfiguration configuration)
        {
            Configuration = configuration;
            ConfigurationHelper.Setup(configuration);

            SyncfusionLicenseProvider.RegisterLicense(AppHelper.SyncfusionLicenceKey);
        }

        public void ConfigureServices(IServiceCollection services)
        {
            Startup.ConfigureServices(services, Configuration); // ###

            AppDICoreHelper.Setup(services);
        }

        // ConfigureServices -> Configure
        public void Configure(IApplicationBuilder applicationBuilder,
            IWebHostEnvironment environment)
        {
            Startup.Configure(applicationBuilder, environment); // ###
        }
    }
}
LazZiya commented 2 years ago

You don't need this line with XLocalizer:

services.AddLocalization(o => o.ResourcesPath = "Resources");

Also I see you have a different path for XLocalizer resources:

.AddXLocalizer<XLocalizerSource>(o =>
{
    o.ResourcesPath = "XLocalizer";
   //...
});

Not sure but this could be making a conflict due to double registering localization settings with different resource folders!

LazZiya commented 2 years ago

also I didn't see app.UseRequestLocalization(); under Configure method:


public static void Configure(IApplicationBuilder applicationBuilder,
            IWebHostEnvironment environment)
{
    // add this line
    app.UseRequestLocalization();
}
EasyLOB commented 2 years ago

Hi, I made the changes you suggested and now it's working ! But I have another question... With the configuration above my "pt-BR" culture messages are shown even if I change the culture of the site to "en-US". How do I show the default "en-US" messages when the culture is "en-US" and my "pt-BR" messages when the culture is "pt-BR" ? Thanks

LazZiya commented 2 years ago

When you set TranslateFromCulture to a culture other than en, then you have to translate all error messages to en in the resource file.