Based on this tutorial I'm trying to have Internationalization capability in my project.
I have these resources files for saving words in different languages:
resources.resx --> for default language (en-US)
resources.fa.resx --> for farsi language
resources.es.resx --> for spanish language
words like fa and es shows the culture.
in views I have replaced words with their equal in resource files in this way :
<a href="#" >#Resources.IranNewsStand</a>
Edit: I've implemented all of the logic based on the tutorial.but I have one view for all of the languages and in this view I'm using resources.resx . Is it a correct logic?
My question is that how my project knows to load which resource file based on the value of Thread.CurrentThread.CurrentCulture ? What did I miss?
Edit: I've implemented these steps:
1-I have a Class Library Project named Resources containing three mentioned resx files(resources.resx,resources.fa.resx,resources.es.resx).
2-Resource project is added in my mvc application as a reference.
3-controllers inherits this Base Controller :
public class BaseController : Controller
{
protected override void ExecuteCore()
{
string cultureName;
HttpCookie cultureCookie = Request.Cookies["_culture"];
if (cultureCookie != null)
cultureName = cultureCookie.Value;
else
cultureName = Request.UserLanguages[0];
cultureName = utilities.CultureHelper.GetValidCulture(cultureName);
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
base.ExecuteCore();
}
}
4-as I said in view I have used resource strings from resource.resx file that contains the default language(en-US) strings:
<p>#Resources.Resources.Home</p>
5-in view I have a link that when clicking on it,this jquery code is run:
<script type="text/javascript">
$("#btnChangeLanguage").click(function () {
$.cookie("_culture", "fa-IR", { expires: 365, path: '/' });
window.location.reload(); // reload
})
</script>
fa-IR is a culture selected by user.but when clicking on this link the language doesn't change.
Edit:Solution
I found 2 problems in my project that solving them made everything ok:
1-jQuery cookie plugin was required to have jquery code work correctely:
<script type="text/javascript" src="~/Scripts/jquery.cookie.js" ></script>
2-the ExecuteCore event in BaseController wouldn't fire and that sounds like it was a problem in asp.net MVC 4 .So based on this question I tryed to override OnActionExecuted instead.
The logic of pulling the resource based on the current culture is built into .NET itself.
There are two steps to this:
The current culture needs to be set appropriately in the request. ASP.NET has some built-in mechanisms to do this, such as what is described in the article you linked to.
Once that is set, the .NET Framework will use the request's current culture to load the appropriate resource. If the requested locale is not available, the culture neutral resource will be loaded (the "fallback").
Related
Just trying to build an Integration Test project for a NET Core Web API.
So I've followed a few examples, including this one (https://dotnetcorecentral.com/blog/asp-net-core-web-api-integration-testing-with-xunit/) and naturally, I run into issues. When I run the simple GET test I get an exception:
"System.InvalidOperationException : The ConnectionString property has not been initialized."
Any help would be appreciated.
For server = new TestServer(new WebHostBuilder().UseStartup<Startup>());, you need to manually configure the appsettings.json path like
var server = new TestServer(WebHost.CreateDefaultBuilder()
.UseContentRoot(#"D:\Edward\SourceCode\AspNetCore\Tests\IntegrationTestMVC")
// This is the path for project which needs to be test
.UseStartup<Startup>()
);
For a convenience way, I would suggest you try Basic tests with the default WebApplicationFactory.
The WebApplicationFactory constructor infers the app content root path by searching for a WebApplicationFactoryContentRootAttribute on the assembly containing the integration tests with a key equal to the TEntryPoint assembly System.Reflection.Assembly.FullName. In case an attribute with the correct key isn't found, WebApplicationFactory falls back to searching for a solution file (*.sln) and appends the TEntryPoint assembly name to the solution directory. The app root directory (the content root path) is used to discover views and content files.
Reference:How the test infrastructure infers the app content root path
I had to override CreateHostBuilder in my derived WebApplicationFactory in order to add the configuration for the connection string (since it was read from user secrets).
public class CustomApplicationFactory : WebApplicationFactory<Sedab.MemberAuth.Startup>
{
protected override IHostBuilder CreateHostBuilder()
{
var initialData = new List<KeyValuePair<string, string>> {
new KeyValuePair<string, string>("ConnectionStrings:DefaultConnection", "test")
};
return base.CreateHostBuilder().ConfigureHostConfiguration(config => config.AddInMemoryCollection(initialData));
}
}
Every example I have found, including the official Microsoft documentation on Localization at https://learn.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.1, uses a Controller to perform the action of setting and saving the desired Culture. My ASP.NET Core 2.1 web app is NOT MVC, so does not have a Controller. I have tried several ways to get around this, including adding a dummy Controller to my project, but I still cannot get the Culture switch to work.
My Startup class Configure method contains the following code:
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("hi-IN")
};
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(DefaultCulture.Name),
// Formatting numbers, dates, etc.
SupportedCultures = supportedCultures,
// UI strings that we have localized.
SupportedUICultures = supportedCultures
});
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc();
The ConfigureServices method contains this code:
// Add the localization services to the services container
services.AddLocalization(options => options.ResourcesPath = "Resources");
// Add MVC Services to the Services Collection.
services.AddMvc()
// Add support for finding localized views, based on file name suffix, e.g. Index.fr.cshtml
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
// Add support for localizing strings in data annotations (e.g. validation messages) via the
// IStringLocalizer abstractions.
.AddDataAnnotationsLocalization();
// Configure supported cultures and localization options
var supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("hi-IN")
};
services.Configure<RequestLocalizationOptions>(options =>
{
// State what the default culture for your application is. This will be used if no specific culture
// can be determined for a given request.
options.DefaultRequestCulture = new RequestCulture(DefaultCulture.Name, DefaultCulture.Name);
// You must explicitly state which cultures your application supports.
// These are the cultures the app supports for formatting numbers, dates, etc.
options.SupportedCultures = supportedCultures;
// These are the cultures the app supports for UI strings, i.e. we have localized resources for.
options.SupportedUICultures = supportedCultures;
});
// Register the email service used for "contacts".
services.AddSingleton<IEmailSender, EmailSender>();
// Configure startup to use the SendGrid options.
services.Configure<AuthMessageSenderOptions>(Configuration);
// Add cross-origin resource sharing services to the specified IServiceCollection.
//
// The Policy specifed as an option will allow any method.
services.AddCors(options => options.AddPolicy("CorsPolicy", b => b.AllowAnyMethod()));
And DefaultCulture is:
DefaultCulture = new CultureInfo(Configuration["Localization:DefaultCulture"]);
Where the settings file contains the string "en-US".
I'm then using the _SelectLanguagePartial.cshtml code from the Localization docs sample:
<div title="#Localizer["Request culture provider:"] #requestCulture?.Provider?.GetType().Name">
<form id="selectLanguage" asp-controller="Home"
asp-action="SetLanguage" asp-route-returnUrl="#returnUrl"
method="post" class="form-horizontal" role="form">
<label asp-for="#requestCulture.RequestCulture.UICulture.Name">#Localizer["Language:"]</label>
<select name="culture" onchange="this.form.submit();" asp-for="#requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems"></select>
</form>
First of all, there is no Controller. How exactly can I implement this functionality in a non-MVC ASP.NET Core web app?
You don’t have to use controllers, recently I’ve published a step-by-step tutorial to develop multicultural web application with ASP.NET Core Razor Pages; you can find it here:
http://www.ziyad.info/en/articles/10-Developing_Multicultural_Web_Application
I used rout value approach, but you can extend it to use query string, cookie or accepted header value for culture selecting.
In the website you can see a live demo and project source code link on github.
Additionally you may need to check localizing identity error messages as well:
http://ziyad.info/en/articles/20-Localizing_Identity_Error_Messages
I hope it helps :)
[UPDATE]
The sample I've provided is using shared resource files. If you want to use view related resource file approatch, create the resource files for each view/culture inside "Resources" folder and keep the folder structure of resources similar to its related views.
for example, if we have a view named "MyView" inside pages folder:
Pages/MyView.cshtml
The resource files should be like below:
Resources/Pages/MyView.tr-TR.resx
Resources/Pages/MyView.ar-SY.resx
Resources/Pages/MyView.hi-IN.resx
To use localization inside views inject IViewLocalizer:
#using Microsoft.AspNetCore.Mvc.Localization
#inject IViewLocalizer _loc
<h4>#_loc["My view title"]</h4>
and for the ViewModel/DataAnnotations, create another resource file for each culture:
View model:
Pages/MyViewModel.cshtml.cs
Resource file name:
Resources/Pages/MyViewModel.tr-TR.resx
Resources/Pages/MyViewModel.ar-SY.resx
Resources/Pages/MyViewModel.hi-IN.resx
Fill the resource files with relevant model property display names and data annotation messages, then modify startup.cs file by clearing the shared resource code for DataAnnotations and keep it parameterless:
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddViewLocalization(o=>o.ResourcesPath = "Resources")
// Option A: use this for localization with shared resource
/*
.AddDataAnnotationsLocalization(o=> {
var type = typeof(ViewResource);
var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
var factory = services.BuildServiceProvider().GetService<IStringLocalizerFactory>();
var localizer = factory.Create("ViewResource", assemblyName.Name);
o.DataAnnotationLocalizerProvider = (t, f) => localizer;
})
*/
// Option B: use this for localization by view specific resource
.AddDataAnnotationsLocalization()
.AddRazorPagesOptions(o => {
o.Conventions.Add(new CultureTemplateRouteModelConvention());
});
btw, I've updated the GitHub sample, now it contains "AnotherPage" view localized using view specific resource files.
I need to create an ASP.NET Core 2 application that can be extensible.
An extension, is a project that reference the main assembly and can extends it adding new controllers, models (with EF migrations), views, razor pages, etc.
Because the extensions need use the main application base classes like base controller, base model or view/page layout, the main application cannot reference the module project (avoid circular references).
I'm not sure how can I achieve this, but the idea is an installation of the main project, can add new functionality simple putting the modules DLL (or by online market in the main application, that download the DLL).
In my research, I found Applications parts, but my problem here is I need specify the part assembly in Startup class, and I need in installed the capacity of install modules without doing any changes in the code.
Some modules, need be extensible too, for example, an accounting module, need to connect with bank, and it have an interface that defines the methods of working with the bank, for example:
public interface IBankingOperation
{
public async void PayInvoiceAsync(Invoice invoice);
// More methods...
}
Then, differents projects can reference the banking assembly and provide implementation for differents banks.
In the main application, this modules can be installed like other modules, but checking the base module is intalled, for example, I can install the Santander module like other module, but only if banking module is installed (module dependency).
In conclusion, I need to create a modular ASP.NET Core 2 application, but the main assembly cannot reference the modules, the modules must reference the main assembly. The modules can contain Controllers, Models, Views, Pages, Etc.
In the main web app you would call a class which loads the extensions in the memory
ModuleManager.LoadModules(Path.Combine(_env.ContentRootPath, "Modules"));
this is the load modules function
public static void LoadModules(string modulesFolder)
{
var appsFolders = new DirectoryInfo(modulesFolder).GetDirectories();
foreach (var appFolder in appsFolders)
{
var binFolder = new DirectoryInfo(Path.Combine(appFolder.FullName, "bin"));
if (!binFolder.Exists)
{
continue;
}
var assemblies = AssemblyProvider.GetAssemblies(binFolder.FullName);
foreach (var assembly in assemblies)
{
var iModuleClass = assembly.GetTypes().FirstOrDefault(type => type.GetInterfaces().Contains(typeof(IModule))
&& type.GetConstructor(Type.EmptyTypes) != null);
if (iModuleClass != null)
{
var module = Activator.CreateInstance(iModuleClass) as IModule;
module.ModuleFolder = appFolder;
Modules.Add(module);
Assemblies.Add(assembly);
break;
}
}
}
}
then you should have an interface which should be implemented by each module the class which implement this interface should do the work of registering services and database models and all staff needed and you will load them as follows
public static IEnumerable<IExtensionRegister> GetModuleRegistrars()
{
lock (Modules)
{
return Modules.Select(item => item.Registrar).Where(item=>item!=null).ToList();
}
}
I'm converting a project to use a satellite assembly.
I created a new class library (named "Resources"). All of the default resources are at the root level *.resx.
Then there is a folder for each culture.
"en/"
with *.en.resx
en-GB/
with *.en-GB.resx
I changed the .resx files Access Modifier to "Public" and changed these settings.
BuildAction: EmbeddedResource
CopyToOutputDirectory: CopyAlways
I made sure the .resx designers *.Designer.cs use the "Resources" namespace.
I added the Class library to an ASP.NET MVC app and in global.asax.cs I have the culture being set as needed.
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-GB");
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
I tried adding the al.exe and Resgen.exe commands to the post-build event in the Resources class library.
"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\Resgen.exe"
en-GB/MyResourceFile.en-GB.resx "C:\Program Files\Microsoft
SDKs\Windows\v7.1\Bin\al.exe" /t:lib
/embed:en-GB/MyResourceFile.en-GB.resources /culture:en-gb
/out:Resources.resources.dll
Looking inside the MVC apps bin/ folder there are folders for each language with 3 files:
MyResources.en-GB.Designer.cs, MyResources.en-GB.resx,
Resources.resources.dll
The root level only has Resources.dll and Resources.resources.dll but the default language works fine. The second *.resources.dll is due to the post-build event.
When I change the CurrentUICulture it doesn't change the UI language. Before moving the resources out of the App_GlobalResources/ to an external assembly it worked fine (Razor example).
#Resources.MyResources.StringIdentifier
Looking at the Resource .dlls using .NET Reflector these are the differences.
Satellite Resources: (Project: Resources)
Resources.en_GB.MyResources.en-GB.resources
App_GlobalResources: (Project MVCApp)
Namespace.MVCApp.App_GlobalResources.MyResources.en-GB.resources
Edit:
After setting the CurrentCulture and UICulture in the global.asax.cs, I tested manually retrieving a ResourceManager which actually worked and retrieved strings in the expected language.
System.Resources.ResourceManager gStrings =
new System.Resources.ResourceManager("Resources.MyResources", System.Reflection.Assembly.Load("Resources")); // Resources=Project Name
string test = gStrings.GetString("TestString"); // WORKS IN GLOBAL.ASAX.CS!!!
I tried the same test in an MVC Controller, but it doesn't work. So the only place the ResourceManager can be retrieved is the Global.asax.cs.
Oddly enough the same test at the UI level in razor would only return the default cultures text! The cultureInfo is correct, but the ResourceManager only returns the default language in the ASP.NET MVC Razor UI. (This is strictly for testing)
#{
System.Globalization.CultureInfo ci = System.Threading.Thread.CurrentThread.CurrentUICulture;
System.Resources.ResourceManager gStrings =
new System.Resources.ResourceManager("Resources.MyResource", System.Reflection.Assembly.Load("Resources"));
string s = gStrings.GetString("Test"); // DOESN'T WORK IN UI
}
TLDR version:
Converting from Resources (.resx) from App_GlobalResources to satellite assemblies.
The issue at hand.
I can manually call a ResourceManager for the satellite assembly, using the CurrentUICulture language in global.asax and it works as expected, however the same test fails from the UI side and fails in a MVC controller.
The default language works fine, so why doesn't the UI switch languages?
If have recently set up a project where the internaltionalization works correctly more or less like you describe.
https://github.com/Jacco/Perpetuality
I switch the language per HTTP request in OnAuthorize in the controller I use as base:
using Perpetuality.Data;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Security.Principal;
using System.Threading;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace Perpetuality.Controllers
{
public partial class BaseController : Controller
{
private CultureInfo GetCultureInfo(AuthorizationContext filterContext)
{
switch ((string)filterContext.RouteData.Values["language"])
{
case "nl":
return CultureInfo.CreateSpecificCulture("nl-NL");
case "en":
return CultureInfo.CreateSpecificCulture("en-US");
case "pt":
return CultureInfo.CreateSpecificCulture("pt-PT");
case "de":
return CultureInfo.CreateSpecificCulture("de-DE");
case "es":
return CultureInfo.CreateSpecificCulture("es-ES");
case "fr":
return CultureInfo.CreateSpecificCulture("fr-FR");
case "it":
return CultureInfo.CreateSpecificCulture("it-IT");
case "jp":
return CultureInfo.CreateSpecificCulture("ja-JP");
default:
return CultureInfo.CreateSpecificCulture("en-US");
}
}
protected override void OnAuthorization(AuthorizationContext filterContext)
{
Thread.CurrentThread.CurrentCulture = GetCultureInfo(filterContext);
Thread.CurrentThread.CurrentUICulture = GetCultureInfo(filterContext);
base.OnAuthorization(filterContext);
}
}
}
In my project the RESX files are specified differently than in yours:
BuildAction: EmbeddedResource
CopyToOutputDirectory: DoNotCopy
In my vies I access the strings like this:
#Resources.Home.Index.FacebookButtonCaption
I did setup nothing post or pre build.
In my project the language is always on the url. That was a user can switch language if no appropriate language was initially chosen.
Where does ASP.NET virtual path resolve the tilde ~ in the links, for example
<link rel="stylesheet" type="text/css" href="~/Css/Site.css" />
Does it redirect, or RedirectToAction in ASP.NET MVC?
It gets it from here:
VirtualPathUtility.ToAbsolute(contentPath, httpContext.Request.ApplicationPath);
Here is the reflector output for PathHelpers class in System.Web.Mvc DLL:
private static string GenerateClientUrlInternal(HttpContextBase httpContext, string contentPath)
{
if (string.IsNullOrEmpty(contentPath))
{
return contentPath;
}
if (contentPath[0] == '~')
{
string virtualPath = VirtualPathUtility.ToAbsolute(contentPath, httpContext.Request.ApplicationPath);
string str2 = httpContext.Response.ApplyAppPathModifier(virtualPath);
return GenerateClientUrlInternal(httpContext, str2);
}
NameValueCollection serverVariables = httpContext.Request.ServerVariables;
if ((serverVariables == null) || (serverVariables["HTTP_X_ORIGINAL_URL"] == null))
{
return contentPath;
}
string relativePath = MakeRelative(httpContext.Request.Path, contentPath);
return MakeAbsolute(httpContext.Request.RawUrl, relativePath);
}
See MSDN:Web Project Paths
ASP.NET includes the Web application
root operator (~), which you can use
when specifying a path in server
controls. ASP.NET resolves the ~
operator to the root of the current
application. You can use the ~
operator in conjunction with folders
to specify a path that is based on the
current root.
Basically, the purpose of the tilde is so that you can have a path that resolves properly even if you deploy your website to different places. Relative paths cannot accomplish this easily because controls may be rendered in different folders within your website. Absolute paths cannot accomplish this because your website may be deployed to different locations -- if nothing else, this is the case for test deployments made locally vs release deployments to the live server.
Server.MapPath can be used for similar reasons.
ASP.Net translates the tilde(~) with the application's root directory in every runat=server control. It is the equivalent for the HttpRuntime.AppDomainAppVirtualPath Property.