Localization with SharedResources not working in .NET Core 2.1

7.9k Views Asked by At

I've been fighting with this problem for hours... and I can't find what it is...

I'm just trying to localize the _Layout.cshtml file. Both the IStringLocalizer and the IHtmlLocalizer do not seem to find the Resource files.

I've followed and searched for: https://github.com/MormonJesus69420/SharedResourcesExample .Net Core Data Annotations - localization with shared resources https://stackoverflow.com/search?q=shared+resources+.net+core https://andrewlock.net/adding-localisation-to-an-asp-net-core-application/

There's something silly that I may be overlooking.

Here's my startup.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using EduPlaTools.Data;
using EduPlaTools.Models;
using EduPlaTools.Services;
using System.Globalization;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc.Razor;
using Pomelo.EntityFrameworkCore.MySql;
using Pomelo.EntityFrameworkCore.MySql.Infrastructure;
using Microsoft.AspNetCore.HttpOverrides;

namespace EduPlaTools
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            // This is for string translation!
            // Adds Localization Services (StringLocalizer, HtmlLocalizer, etc.)
            // the opts.ResourcesPath = is the path in which the resources are found.
            // In our case the folder is named Resources!
            // There's specific and neutral resources.  (Specific en-US). (Neutral: es)
            /**
             * If no ResourcesPath is specified, the view's resources will be expected to be next to the views.
             * If ResourcesPath were set to "resources", then view resources would be expected to be ina  Resource directory,
             * in a path speicifc to their veiw (Resources/Views/Home/About.en.resx, for example).
             * 
             * */
            services.AddLocalization(opts => opts.ResourcesPath = "Resources");

            // services.AddBContext
            // There are subtle differences between the original and the modified version.

            services.AddDbContextPool<ApplicationDbContext>(options =>
                options.UseMySql(Configuration.GetConnectionString("MySQLConnection"),
                mysqlOptions =>
                {
                    mysqlOptions.ServerVersion(new Version(8, 0, 12), ServerType.MySql); // replace with your Server Version and Type
                }
                ));
                //options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            // Add application services.
            services.AddTransient<IEmailSender, EmailSender>();




            services.AddMvc()
                    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, options => options.ResourcesPath = "Resources")
                    .AddDataAnnotationsLocalization();

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            // This may be dangerous and is not recommended
            using (var serviceScope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>()
                    .CreateScope())
            {
                serviceScope.ServiceProvider.GetService<ApplicationDbContext>()
                     .Database.Migrate();
            }

            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });

            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }


            // These must line up with the ending of the .resx files.
            // Example: SharedResources.en.resx, SharedResources.es.rex

            // If you want to add specific, then do it like:
            // new CultureInfo("en-US")
            List<CultureInfo> supportedCultures = new List<CultureInfo>
            {
                new CultureInfo("es"),
                new CultureInfo("en"),
                new CultureInfo("es-ES"),
                new CultureInfo("en-US")
            };

            // Registers the localization, and changes the localization per request.
            app.UseRequestLocalization(new RequestLocalizationOptions
            {
                // We give the default support of Spanish.
                DefaultRequestCulture = new RequestCulture("es"),
                // Format numbers, dates, etc.
                SupportedCultures = supportedCultures,
                // The strings that we have localized
                SupportedUICultures = supportedCultures
            });

            // This will seed the databse:

            SeedDatabase.Initialize(app.ApplicationServices);

            app.UseStaticFiles();


            app.UseAuthentication();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Here's how I'm trying to call it inside the _Layout.cshtml:

@using  Microsoft.AspNetCore.Mvc.Localization

@inject IViewLocalizer Localizer
@inject IStringLocalizer<SharedResources> SharedLocalizer
@inject IHtmlLocalizer<SharedResources> _localizer;

@SharedLocalizer["Menu_Home"]

Here's the directory structure:

enter image description here

Here are the contents of SharedResources.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace EduPlaTools
{
    /**
     * This is a dummy class that is needed so Localization works.
     * Now in .NET Core Localization works as a service, and implementsw
     * naming conventions (AT the file level). Therefore, if the files do not
     * implement the correct name, there's going to be problems. 
     * 
     * See an example, here:
     * https://github.com/SteinTheRuler/ASP.NET-Core-Localization/blob/master/Resources/SharedResources.cs
     *
     * This is a workaround to create a Resource File that can be read by the entire
     * application. It's left in blank so the convention over configuration
     * picks it up.
     * 
     * */
    public class SharedResources
    {
    }
}

Here are the contents of the resx files:

enter image description here

enter image description here

I've also tried renaming them to no avail.. (Tried Resources.es.rex, Resources.rex)

I tried setting breakpoints to see how it behaved. It of course, didn't find the Resource files. I then compared it with Mormon's repo by recalling an inexistent key. I compared it with my output, but Mormon's repo doesn't display the "SearchedLocation" (Was it introduced in a later .NET Core version?)

Mormon's Repo: enter image description here

My repo: enter image description here

I know this may be something silly... But it's been close to 4 hours, and I can't stop since I have a LOT to do!!

Any ideas?

3

There are 3 best solutions below

3
On BEST ANSWER

if you want to implement localization with shared resource, you have to create your own culture localizer class:

public class CultureLocalizer
    {
        private readonly IStringLocalizer _localizer;
        public CultureLocalizer(IStringLocalizerFactory factory)
        {
            var type = typeof(ViewResource);
            var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
            _localizer = factory.Create("ViewResource", assemblyName.Name);
        }

        // if we have formatted string we can provide arguments         
        // e.g.: @Localizer.Text("Hello {0}", User.Name)
        public LocalizedString Text(string key, params string[] arguments)
        {
            return arguments == null
                ? _localizer[key]
                : _localizer[key, arguments];
        }
    }

then register it is startup:

services.AddSingleton<CultureLocalizer>();

and modify view locaization settings :

services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
            .AddViewLocalization(o=>o.ResourcesPath = "Resources")

in your views you have to inject the culture localizer class before using it.

those are initial settings for view localization with shared resource, you need to configure localization settings for DataAnnotation, ModelBinding and Identity error messages as well.

these articles could help for starting:

Developing multicultural web application with ASP.NET Core 2.1 Razor Pages:

http://www.ziyad.info/en/articles/10-Developing_Multicultural_Web_Application

it includes step by step tutorial for localizing using shared resources, additionally, this article is about localizing Identity error messages :

http://ziyad.info/en/articles/20-Localizing_Identity_Error_Messages

0
On

I wanted to add an answer which further develops Laz's solution. Just in case someone wants to have individual localized views.

Back in Startup.cs, you have:

services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
            .AddViewLocalization(o=>o.ResourcesPath = "Resources")

Technically, you are indicating MVC to look in the "Resources" folder as the main path, and then follow the convention to look for localized resource files.

Therefore In case you want to localize the Login.cshtml view found in Views/Account/Login.chsmtl, you have to create the resource file in: Resources/Views/Account/Login.en.resx

You would then need to add the following either in the view directly Login.cshtml or in the _ViewImports.cshtml to reference it to all the views:

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer

After that, in your code you can do: Localizer["My_Resource_file_key"]

And you'll have it translated.


Here are some illustrations:

Folder Structure for the Individual Localization

Localized View

0
On

An update to the previous answers. Due to the recent breaking change in .NET Core 3 (https://github.com/dotnet/docs/issues/16964), the accepted answer will only work if the resource lives directly in the resource folder. I have created a workaround to use shared resources in views (same applies to controllers, data annotations, services, whatever you need...).

First you need to create an empty class for your resources. This one has to live under YourApp.Resources namespace. then create your resources named same as your class (in my example I have Views.cs in the namespace MyApp.Resources.Shared and Views.resx).

Then here is the helper class to load the shared resources:

public class SharedViewLocalizer
{
    private readonly IStringLocalizer _localizer;

    public SharedViewLocalizer(IStringLocalizerFactory factory)
    {
        var assemblyName = new AssemblyName(typeof(Resources.Shared.Views).GetTypeInfo().Assembly.FullName);
        localizer = factory.Create("Shared.Views", assemblyName.Name);
    }

    public string this[string key] => _localizer[key];

    public string this[string key, params object[] arguments] => _localizer[key, arguments];
}

You have to register is in the Startup.Configure:

services.AddSingleton<SharedViewLocalizer>();

I suppose you use

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

to setup default resources location.

And then in your view you use it as follows:

@inject IViewLocalizer _localizer
@inject SharedViewLocalizer _sharedLocalizer

@_localizer["View spacific resource"] // Resource from Resources/Views/ControllerName/ViewName.resx
@_sharedLocalizer["Shared resource"] // Resource from Resources/Shared/Views.resx
@_sharedLocalizer["Also supports {0} number of arguments", "unlimited"]

Same principle can be applied to DataAnnotations where we can use the built-in method in Startup.Configure:

services.AddMvc()
    .AddDataAnnotationsLocalization(options => 
    {
        options.DataAnnotationLocalizerProvider = (type, factory) =>
        {
           var assemblyName = new AssemblyName(typeof(DataAnnotations).GetTypeInfo().Assembly.FullName);
           return factory.Create("Shared.DataAnnotations", assemblyName.Name
        };
    })
    .AddViewLocalization();

Again, I'm expecting my resources to live in the namespace Resources.Shared and have an empty class called DataAnnotations created.

Hope this helps to overcome the current breaking change problems.