I have a .NET 6 Bloazor PWA application where i want to let the customer choose the language for the UI of the app.
I have prepared resource .resx files and set the Default language in Program.cs and set the AddLocalization service, but when i chose the language from the combo element the localized text on the page do not change at all.
Moreover i always see the 'keys' instead their corresponding strings in resurces file; it looks like the system do to find resource files at all.
where am i wrong ?
I used:
(.csproj file)
<PropertyGroup>
<BlazorWebAssemblyLoadAllGlobalizationData>true</BlazorWebAssemblyLoadAllGlobalizationData>
</PropertyGroup>
(Program.cs):
builder.Services.AddLocalization(Options => Options.ResourcesPath= "ResourceFiles");
var host = builder.Build(); CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("it-IT");
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo("it-IT");
await host.RunAsync();
then i defined the ResourceFiles directory
with inside Resource.resx , Resources.it.resx with two text records with
(Resources.it.resx and Resources.resx)
Name="intro1" value="Benvenuto!"
Name="intro2" value="Grazie per utilizzare la nostra app" `
(Resources.en.resx)
Name="intro1" value="Welcome!"
Name="intro2" value="Thank you for using our app" `
and in addition an empty Resources class to collect resx files and assign later it to IStringLocalizer in the razor page
namespace BlazorAppPWAalone.ResourceFiles
{
public class Resources
{
}
}
in my page:
#using System.Globalization
#using Microsoft.Extensions.Localization
#inject IStringLocalizer<Resources> Loc
<h1>Language change:</h1>
<select name="lingue" id="idlingua" #bind="sceltaLingua">
<option value="it-IT">Italiano</option>
<option value="en-US">Inglese</option>
</select>
<p>
<b>CurrentCulture</b>: #CultureInfo.CurrentCulture
<b>CurrentUICulture</b>: #CultureInfo.CurrentUICulture
</p>
<p>#Loc["intro1"], #Loc["intro2"]</p>
#code {
private string _sceltaLingua;
public string sceltaLingua
{
get
{
return _sceltaLingua;
}
set
{
_sceltaLingua = value;
CambiaLingua(_sceltaLingua);
}
}
private void CambiaLingua(string newlang)
{
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(newlang);
System.Threading.Thread.CurrentThread.CurrentCulture = new CultureInfo(newlang);
StateHasChanged();
}
}
When i choose the language the #CurrentCulture and #CurrentUICulture variables show correctly the selected language but #Loc["intro1"], #Loc["intro2"] never change, it always displays default (italian) resource values.
Why ? where is my code 'bug' ?
I´ve not done this in WASM yet but in the documentation the CultureSelector does not actually set CurrentCulture to change the language but sets a key value pair in the browsers local storage and triggers a reload which causes DefaultThreadCurrentCulture and
DefaultThreadCurrentUICulture to be set to the culture specified in local storage.
And judging from this:
Always set DefaultThreadCurrentCulture and DefaultThreadCurrentUICulture to the same culture in order to use IStringLocalizer and IStringLocalizer.
just setting CurrentCulture and CurrentUICulture will probably not work.
I don´t know if you can just set DefaultThreadCurrentCulture and DefaultThreadCurrentUICulture from the component and reload or if you need to follow the documentations exact approach but that should be easy for you to try out.
Related
I'm working on adding localization to my web application. I have configured the IStringLocalizer and it is correctly reading string resources from two different resx files, depending on the browser setting. It then maps those string resources to ViewData, from which my View is getting text in correct language (not sure if that is the best approach, but for now I don't want to spent more time on this).
The thing is that I also have a drop down list in my UI, that allows users to manually switch language. I'm reading the value set by user in my controller action and adding it to cookies, but now I'd also like to set my applications' culture to the one matching the string in cookie.
Is it possible to set application culture from the controller action in MVC Core? If yes, then how to do this correctly?
EDIT:
I have just learned that I can do something like this:
<a class="nav-item nav-link" asp-route-culture="en-US">English</a>
and it will add ?culture=en-US to my route, which will set culture for the page for me. Is there any way to do the same without having to keep it in an address bar?
EDIT 2:
Regarding answer by Adam Simon:
CookieRequestCultureProvider is what I'd like to use in my app, but the problem is that it is not producing any cookie. Documentation says that .net core will resolve which provider to use by checking which will give a working solution, starting from QueryStringRequestCultureProvider, then going to CookieRequestCultureProvider, then other providers.
My current Startup looks like this:
public class Startup
{
private const string defaultCulutreName = "en-US";
public void ConfigureServices(IServiceCollection services)
{
services.AddLocalization();
services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[]
{
new CultureInfo(defaultCulutreName),
new CultureInfo("pl-PL")
};
options.DefaultRequestCulture = new RequestCulture(defaultCulutreName, defaultCulutreName);
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles();
//TRIED PLACING IT BEFORE AND AFTER UseRequestLocalization
//CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("pl-PL", "pl-PL"));
app.UseRequestLocalization(app.ApplicationServices
.GetService<IOptions<RequestLocalizationOptions>>().Value);
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("pl-PL", "pl-PL"));
app.UseMvc(ConfigureRoutes);
}
private void ConfigureRoutes(IRouteBuilder routeBuilder)
{
routeBuilder.MapRoute("Default", "{controller=About}/{action=About}");
}
}
Regarding CookieRequestCultureProvider.MakeCookieValue(new RequestCulture("pl-PL", "pl-PL")) I have tried putting it in RequestLocalizationOptions in ConfigureServices, in Configure before UseRequestLocalization and after that. All with the same result.
The following "problems" appear with this solution:
MakeCookieValue method is not producing any .AspNetCore.Culture cookie
Chrome browser with language set to PL is using pl-PL culture
correctly, yet Firefox is using en-US culture with language set to PL
in options (despite commenting out options.DefaultRequestCulture =
new RequestCulture(defaultCulutreName, defaultCulutreName) line)
Somehow my localization is working by default without using query
strings nor cookies to provide culture for application, but this is
not how I'd like it to work, as I do not have any control over it
Somehow you must tie the selected culture to the user so if you don't want to carry it around in the URL, you must find another way to retain this piece of information between requests. Your options:
cookie
session
database
HTTP header
hidden input
Under normal circumstances using a cookie to store the language preference is a perfect choice.
In ASP.NET Core the best place to retrieve and set the culture for the current request is a middleware. Luckily, the framework includes one, which can be placed in the request pipeline by calling app.UseRequestLocalization(...) in your Startup.Configure method. By default this middleware will try to pick up the current culture from the request URL, cookies and Accept-Language HTTP header, in this order.
So, to summarize: you need to utilize the request localization middleware, store the user's culture preference in a cookie formatted like c=%LANGCODE%|uic=%LANGCODE% (e.g. c=en-US|uic=en-US) and you are done.
You find all the details in this MSDN article.
Bonus:
It then maps those string resources to ViewData, from which my View is
getting text in correct language (not sure if that is the best
approach, but for now I don't want to spent more time on this).
Passing localized texts to views in ViewData is cumbersome and error-prone. In ASP.NET Core we have view localization for this purpose. You just need to inject the IViewLocalizer component into your views to get a nice and convenient way to access your localized text resources. (Under the hood IViewLocalizer uses IStringLocalizer.)
About EDIT 2
MakeCookieValue method is not producing any .AspNetCore.Culture cookie
CookieRequestCultureProvider.MakeCookieValue method is just a helper to generate a cookie value in the correct format. It just returns a string and that's all. But even if it were meant to add the cookie to the response, calling it in Startup.Configure would be completely wrong as you configure the request pipeline there. (It seems to me you're a bit confused about request handling and middlewares in ASP.NET Core so I suggest studying this topic.)
So the correct setup of the request pipeline is something like this:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
#region Localization
// REMARK: you may refactor this into a separate method as it's better to avoid long methods with regions
var supportedCultures = new[]
{
new CultureInfo(defaultCultureName),
new CultureInfo("pl-PL")
};
var localizationOptions = new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(defaultCultureName, defaultCultureName),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures,
// you can change the list of providers, if you don't want the default behavior
// e.g. the following line enables to pick up culture ONLY from cookies
RequestCultureProviders = new[] { new CookieRequestCultureProvider() }
};
app.UseRequestLocalization(localizationOptions);
#endregion
app.UseStaticFiles();
app.UseMvc(ConfigureRoutes);
}
(A remark on the above: it's unnecessary to register RequestLocalizationOptions in the DI container.)
Then you can have some controller action setting the culture cookie:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult SetCulture(string culture, string returnUrl)
{
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
// making cookie valid for the actual app root path (which is not necessarily "/" e.g. if we're behind a reverse proxy)
new CookieOptions { Path = Url.Content("~/") });
return Redirect(returnUrl);
}
Finally, an example how to invoke this from a view:
#using Microsoft.AspNetCore.Localization
#using Microsoft.AspNetCore.Http.Extensions
#{
var httpContext = ViewContext.HttpContext;
var currentCulture = httpContext.Features.Get<IRequestCultureFeature>().RequestCulture.UICulture;
var currentUrl = UriHelper.BuildRelative(httpContext.Request.PathBase, httpContext.Request.Path, httpContext.Request.QueryString);
}
<form asp-action="SetCulture" method="post">
Culture: <input type="text" name="culture" value="#currentCulture">
<input type="hidden" name="returnUrl" value="#currentUrl">
<input type="submit" value="Submit">
</form>
Chrome browser with language set to PL is using pl-PL culture correctly, yet Firefox is using en-US culture with language set to PL in options (despite commenting out options.DefaultRequestCulture =
new RequestCulture(defaultCulutreName, defaultCulutreName) line)
I suspect Chrome browser sends the language preference in the Accept-Language header while FF not.
Somehow my localization is working by default without using query strings nor cookies to provide culture for application, but this is not how I'd like it to work, as I do not have any control over it
I repeat:
By default this middleware will try to pick up the current culture from the request URL, cookies and Accept-Language HTTP header, in this order.
You can configure this behavior by changing or replacing the RequestLocalizationOptions.RequestCultureProviders list.
I use dotnetbrowser to display a web browser on a old windows framework.
have you an idea to define the download path ?
My dotnetbroser is enable, i can show my webpage but i don't found in documentation or exemple how define this simple download path.
The only exemple that i've found is about the download event detection.
I use WPF in C#
Thanks.
The DotNetBrowser.DownloadItem.DestinationFile property is writable and can be used to configure the path to store the file.
To set this property in your application, you need to subclass the DotNetBrowser.DefaultDownloadHandler and implement its AllowDownload(DownloadItem) method. Then you need to configure your download handler as shown in the documentation article: File Download
You can also configure and use DotNetBrowser.WPF.WPFDefaultDownloadHandler instance to show file chooser and select the path to store the file.
This is a solution
Défine your browser like variable :
BrowserView myBrowserView;
Browser myBrowser;
Create the browser properly :
this.myBrowser = BrowserFactory.Create();
this.myBrowserView = new WPFBrowserView(this.myBrowser);
Create event detection for download
this.myDowloadHandler = new SampleDownloadHandler();
this.myBrowser.DownloadHandler = myDowloadHandler;
Add it to a container, here, a grid
grid_navigateur.Children.Add((UIElement)myBrowserView.GetComponent());
Now we are going to use our "SampleDownloadHandler" class
class SampleDownloadHandler : DownloadHandler
{
public bool AllowDownload(DownloadItem download)
{
download.DestinationFile = "exemple\of\path\whith\file\name";
download.DownloadEvent += delegate(object sender, DownloadEventArgs e)
{
DownloadItem downloadItem = e.Item;
if (downloadItem.Completed)
{
System.Windows.MessageBox.Show("Download complete");
}
};
return true;
}
My personalisated class define path and name of the file who is download and pop a message when is over.
(to found the file name, you do to cut the string download.DestinationFile after the last )
Simple. I want to localize my application.
I've googled for days, and tried a million different approaches to reach my resources.
The only way I've been succesful is by using the standard asp.net folders "App_LocalResource", making the resource files public and giving them a Custom Tool Name. In the view I can then import the Custom Tool Name with #using.
My issue is that the language/resource items arent changing when I change the culture.
Here is how I change it in global.asax:
protected void Application_AcquireRequestState(object sender, EventArgs e)
{
if (HttpContext.Current.Session != null)
{
CultureInfo ci = (CultureInfo)this.Session["Culture"];
if (ci == null)
{
string langName = "en";
string autoLang = "";
if (HttpContext.Current.Request.UserLanguages != null && HttpContext.Current.Request.UserLanguages.Length != 0)
{
autoLang = HttpContext.Current.Request.UserLanguages[0].Substring(0, 2);
}
if (autoLang == "da")
langName = autoLang;
ci = new CultureInfo(langName);
this.Session["Culture"] = ci;
}
Thread.CurrentThread.CurrentUICulture = ci;
Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(ci.Name);
}
}
So the culture is either da or en. But I noticed that the names of the resource files has to have a specific syntax. There has to be a default (in this case english) with no country/culture code and other that default has be named like reFile.da-DK.resx. It has to have both language and culture code.
I'm affraid the resource handler can recognize my file, because culture is set to "da" and not "da-DK". If I name my da resouce file to resFile.da.resx I cant import the Custom Tool Name which is my resouce files.
What do I do to solve this?
Use the full culture info string, ex:
var info = new CultureInfo("en-US")
Also for best practice move the code out into the Application_BeginRequest method, that's the standard location you'll see this type of code.
I'm sending an email from my ASP.NET MVC app using the Spark View Engine based on this example by Andrew Kharlamov.
I've setup a unit test, CanSendEmail, but I need to specify the viewfolder in the config.
I found the documentation here and the examples give this:
<spark>
<views>
<add name="{any-unique-name}"
folderType="FileSystem|EmbeddedResource|VirtualPathProvider|Custom"
type="{name, assembly of IViewFolder type}"
constuctor-param-names="values"
subfolder="{optional subfolder to target}"/>
</views>
</spark>
My question is this. Which folderType do I use and do I need any other parameters. My test product is call myProject.Tests and my web project containing the views is called myProject.Web with a Views folder in it.
Do I use FileSystem, VirtualPathProvider ... ?
Edit [14/11/2011]:
Okay I've got this in my app.config in myProject.Tests:
<views>
<add name="web-view-folder"
folderType="VirtualPathProvider"
virtualBaseDir="~/Views"/>
</views>
I still get "View source file not found." when I run my test. I want the test to use the Views in myproject.Web.
My Solution
Based on the blog posts here and here, and with help from #RobertTheGrey and looking at the tests in the Spark source code, I ended up using ViewFolderType.FileSystem. That worked.
Here's the my code under test:
public string RenderEmailWithCustomViewFolder(string sparkViewName, ViewDataDictionary viewData, Dictionary<string, string> viewFolderParameters)
{
var settings = new SparkSettings()
.SetPageBaseType(typeof (SparkView))
.AddViewFolder(ViewFolderType.FileSystem, viewFolderParameters)
.AddAssembly("MvcContrib");
var engine = new SparkViewEngine(settings);
var sparkViewDescriptor = new SparkViewDescriptor().AddTemplate(sparkViewName);
var view = (SparkView)engine.CreateInstance(sparkViewDescriptor);
try
{
// Merge view data
viewData.Keys.ToList().ForEach(x => view.ViewData[x] = viewData[x]);
// Render the view to a text writer
var writer = new StringWriter();
view.RenderView(writer);
return writer.ToString();
}
finally
{
engine.ReleaseInstance(view);
}
}
And here's my test:
[Test]
public void Can_Render_Order_Confirmation_Email_With_Spark_View_Engine()
{
// Arrange
var order = OrderInstanceFactory.CreateTestOrder();
order.ContactEmail = "test#testicle.com";
var emailService = new EmailService();
var viewData = new ViewDataDictionary();
viewData["Order"] = order;
const string viewFolder = #"../../../../app/myProject.Web/Views";
var viewFolderParameters = new Dictionary<string, string> {{"basePath", viewFolder}};
// Act
var emailBody = emailService.RenderEmailWithCustomViewFolder("Email/OrderConfirmation.spark", viewData, viewFolderParameters);
// Assert
Assert.IsNotNull(emailBody);
Assert.IsTrue(emailBody.Contains("test#testicle.com"));
}
My OrderConfirmation.spark template lives in my web products in the Views/Email/.
If it's an ASP.NET MVC app, then you can use VirtualPathProvider since that hooks into the HttpContext and the rest of the runtime. You would use a FileSystemProvider if you were runnig it from a console app for example, or if you wanted to add a folder from outside your web app, perhaps because the templates were shared by other apps, but I've rarely seen that done.
Hope that helps...
I am dynamically downloading a XAP file that has an embedded resource assembly, with a single resource file (ApplicationStrings.fr-CA.resx). I am using WebClient to pull down the XAP file and using the following code to load the assembly, based on work done by Jeff Prosise in this post: http://www.wintellect.com/CS/blogs/jprosise/archive/2010/06/21/dynamic-localization-in-silverlight.aspx.
Note that I also manually create the XAP file from the fr-CA folder with assembly and the ApplicationManifest.xaml, as described by Guy Smith-Ferrier's steps listed in his presentation here http://www.guysmithferrier.com/post/2010/10/Building-Localized-XAP-Resource-Files-For-Silverlight-4.aspx.
// Get the application manifest from the downloaded XAP
StreamResourceInfo sri = new StreamResourceInfo(e.Result, null);
XmlReader reader = XmlReader.Create(Application.GetResourceStream(sri, new Uri("AppManifest.xaml", UriKind.Relative)).Stream);
AssemblyPartCollection parts = new AssemblyPartCollection();
// Enumerate the assemblies in the downloaded XAP
if (reader.Read())
{
reader.ReadStartElement();
if (reader.ReadToNextSibling("Deployment.Parts"))
{
while (reader.ReadToFollowing("AssemblyPart"))
{
parts.Add(new AssemblyPart() { Source = reader.GetAttribute("Source") });
}
}
}
// Load the satellite assemblies
foreach (AssemblyPart part in parts)
{
if (part.Source.ToLower().Contains("resources"))
{
Stream assembly = Application.GetResourceStream(sri, new Uri(part.Source, UriKind.Relative)).Stream;
part.Load(assembly);
}
}
// Change the culture
Thread.CurrentThread.CurrentCulture = culture;
Thread.CurrentThread.CurrentUICulture = culture;
The assembly seems to load ok, and I have matched up namespaces with the default resource file (ApplicationStrings.resx) with the downloaded resource file (ApplicationStrings.fr-CA.resx). As seen the code, the culture is set for the current thread.
However, calls to ApplicationStrings.ResourceManager.GetString(...) do not return the resources for the set culture. For example, the following should return a string for the new culture (fr-CA), but always returns the default culture (en-US).
/// <summary>
/// Looks up a localized string similar to User Name:.
/// </summary>
public static string Label_UserName {
get {
return ResourceManager.GetString("Label_UserName", resourceCulture);
}
}
Any suggestions? Thanks.
** UPDATE
I figured it out...I had forgotten to reset my supported locals in my satellite assembly project file:
<SupportedCultures>fr-CA</SupportedCultures>
I also made my folder structure exactly as it is for the default resources in my main Silverlight application.