I'm trying to localize my Blazor app (.NET 5.0) using IStringLocalizer and user UI selection based on cookies. Following the documentation, it seems to work if I create a .resx file for each page in my Resources/Pages folder.
I'd like to group all key-value pairs within a single file as follows :
MyApp
|-- Resources
| MyResources.resx <--- set to public modifiers to generate class!!!
| MyResources.es.resx
| MyResources.fr.resx
...
|-- Resources
| Index.razor
So in Startup.cs I register the Resources folder:
services.AddControllers();
services.AddLocalization(options => options.ResourcesPath = "Resources");
I also added MapControllers in the configure method, as well as registering supported cultures. I also added the controller:
[Route("[controller]/[action]")]
public class CultureController : Controller {
public IActionResult SetCulture(string culture, string redirectUri) {
if (culture != null) {
HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)));
}
return LocalRedirect(redirectUri);
}
}
Now, on my main "index.razor" page I inject the IStringLocalizer<MyResources> as follows:
#page "/index"
#inject IStringLocalizer<Language> _localizer
<h2>#_localizer["hello_world"]</h2>
<h2>#DateTime.Now.ToShortDateString()</h2>
<h2>#DateTime.Now.ToLongDateString()</h2>
I make sure that all resx files actually contain the "hello_world" key with some values and added a language selector.
When I run the app, changing the language does change the displayed dates, however, the hello_world key is not found so the app displays the key instead. When exploring the _localizer variable, I see that the container is empty - none of the key-value pairs of the .resx are actually loaded.
Is it possible to get this to work? If so, what am I doing wrong here?
The mistake is because of naming here:
#inject IStringLocalizer<Language> _localizer
should be
#inject IStringLocalizer<MyResources> _localizer
And important is to add an empty file MyResources.razor at the root of the project.
Edit:
Another mistake I made is to add the myApp.Resources to _Imports.razor
...
#using myApp.Resources <==== do NOT add this
#neggenbe
Adding an empty razor(AppLang.razor) component at the root of my project with same name with my resource(AppLang.resx) file solved the challenge. I stumbled accross #neggenbe after over 48 hours trying to figure out what was wrong with my code.
If your project is purely blazor, you wont come accross this challenge but if its a mix of MVC and razor(blazor) components, then you'll definately need to do this.
Related
I'm trying to use the new Razor SDK to include my views within my class libraries where each class library is an MVC Area. If I include the views with an Areas directory in my class library e.g.
/MyLibrary/Areas/MyLibrary/Views/Home/Index.cshtml
Then it loads fine. However I don't like that I have to place them inside an Areas directory ideally the path would be:
/MyLibrary/Views/Home/Index.cshtml
I'm guessing I would have to use a view location expander to achieve this. However I don't know how to achieve this for a view which is contained within a class library and not within the application. So far I have come up with:
public class AreaViewLocationExpander : IViewLocationExpander {
public virtual IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations) {
return viewLocations.Concat(new[] {
"../{2}/Views/{1}/{0}" + RazorViewEngine.ViewExtension
});
}
public virtual void PopulateValues(ViewLocationExpanderContext context) { }
}
This throws the error:
InvalidOperationException: The view 'Index' was not found. The
following locations were searched:
/Areas/MyLibrary/Views/Home/Index.cshtml
/Areas/MyLibrary/Views/Shared/Index.cshtml
/Views/Shared/Index.cshtml
/Pages/Shared/Index.cshtml
/../MyLibrary/Views/Home/Index.cshtml
But I'd imagine even if it did work locally, it wouldn't in production or when the library is packaged up in a NuGet package.
I'd appreciate it if someone could show me how this can be achieved. Thanks
Step 1
From #pranavkm on GitHub:
The Razor Sdk uses well-known MSBuild metadata to calculate project
relative paths. You may set the Link metadata for a file and Razor
would use that. For instance, adding this to your project file would
update all cshtml files to have a view engine path with the project
name as a prefix:
<Target Name="UpdateTargetPath" BeforeTargets="AssignRazorGenerateTargetPaths">
<ItemGroup>
<RazorGenerate Include="#(RazorGenerate)" Link="$(TargetName)\%(RazorGenerate.RelativeDir)%(RazorGenerate.FileName)%(RazorGenerate.Extension)" />
</ItemGroup>
</Target>
This does not work with runtime compilation - i.e. if you were to
apply this to Application.csproj, it'll work for build time compiled
views, but runtime compiled views would continue to use
/Views/Home/Index.cshtml
Step 2
You would then need to add the following IViewLocationExpander:
public class AreaViewLocationExpander : IViewLocationExpander {
public virtual IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations) {
return viewLocations.Concat(new[] {
"/{2}/Views/{1}/{0}" + RazorViewEngine.ViewExtension
});
}
public virtual void PopulateValues(ViewLocationExpanderContext context) { }
}
I have a custom there, where I try to require some of my css and js files via the ResourceManifest.cs file - I keep into running a quite weird issue tough.
I get the following error:
a 'script' named 'FoundationScript' could not be found
This is my ResourceManifest.cs:
using Orchard.UI.Resources;
namespace Themes.TestTheme
{
public class ResourceManifest : IResourceManifestProvider
{
public void BuildManifest(ResourceManifestBuilder builder)
{
var manifest = builder.Add();
manifest.DefineStyle("Foundation").SetUrl("foundation.min.css");
manifest.DefineScript("FoundationScript").SetUrl("foundation.min.js");
}
}
}
In the Layout.cshtml, I have following:
#{
Script.Require("ShapesBase");
Script.Require("FoundationScript");
Style.Include("site.css");
Style.Require("Foundation");
}
What am I missing here?
The issue here is, that the project Themes has a problem with the dynamic compile mechanism of Orchard (i don't know what is wrong exactly) because it resides in folder Themes. Even if you define a class inside the Themes assembly, it will result in an error telling you there is no such class in that assembly.
solution :
Try re-generating your theme with /CreateProject:true and /IncludeInSolution:true parameters as follows:
codegen theme TestTheme /CreateProject:true /IncludeInSolution:true /BasedOn :TheThemeMachine
It will create your theme in a separate project and orchard will pick your registered ResourceManifest.
Hope this helps.
I'm following the spinner from monodroid tutorial. But encountered problem on the resource.
It cannot lookup the SimpleSpinnerItem & SimpleSpinnerDropDownItem on VS 2010.
Am I missing something?
Edit: Create a partial class to register android runtime as per jonp
public partial class Resource
{
public partial class Layout
{
[Register("simple_spinner_dropdown_item")]
public const int SimpleSpinnerDropDownItem = 17367049;
[Register("simple_spinner_item")]
public const int SimpleSpinnerItem = 17367048;
}
}
Edit 2: Tried the global resource
Edit 3: Conflict on my project namespace
I already identified why the const cannot be recognize. It's because of my namespace projectname.Android, it's being duplicated. When I changed it to projectname.AndroidMobile the global resource is there.
See the conflict below.
Also, to avoid the conflict just use the global:: as per jonp
You need to qualify the class, as there are two Resource types: one local to your project (Your.Namespace.Resource, located in Resource.designer.cs), and global::Android.Resource. You need to use global::Android.Resource.Layout.SimpleSpinnerItem.
I have MVC areas in external libraries which have their own area registration code just as a normal MVC area would. This area registration gets called for each dll (module) and I have verified the RouteTable contains all the routes from the loaded modules once loading has been completed.
When I reference these external areas in the main site they get pulled into the bin directory and load up fine. That is, when a request is made for a route that exists in an external library, the correct type is passed to my custom controller factory (Ninject) and the controller can be instantiated.
Once I move these dll's outside of the bin directory however (say to a Modules folder), there appears to be an issue with routing. I have checked that the RouteTable has all the required routes but by the time a request makes its way into the ninject controller factory the requested type is null. From reading here an SO link here this behaviour seems to occur when ASP.NET MVC cannot find the controller matching the requested route or does not know how to make sense of the route.
When loading the modules externally I have ensured that the modules that I want loaded are loaded into the app domain via a call to Assemby.LoadFrom(modulePath);
I did some research and it appears that when attempting to load a library outside of bin you need to specify private probing in app.config as pointed out here;. I have mine set to 'bin\Modules' which is where the mvc area modules get moved too.
Does anyone have any ideas why simply moving an mvc area project outside of the bin folder would cause the requested type passed into the controller factory to be null resulting in the controller to be instantiated?
Edit:
All routes registered in external areas have the namespace of the controller specified in the route
Below is a fragment of code that creates a new Ninject kernel, reads a list of module names from a file to enable, and then goes searching for the enabled modules in the bin/Modules directory. The module is loaded via the assembly loader, has its area(s) registered and then loaded into the ninject kernel.
// comma separated list of modules to enable
string moduleCsv = ConfigurationManager.AppSettings["Modules.Enabled"];
if (!string.IsNullOrEmpty(moduleCsv)) {
string[] enabledModuleList = moduleCsv.Replace(" ", "").Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
_Modules = enabledModuleList ?? new string[0];
// load enabled modules from bin/Modules.
var moduleList = Directory.GetFiles(Server.MapPath("~" + Path.DirectorySeparatorChar + "bin" + Path.DirectorySeparatorChar + "Modules"), "*.dll");
foreach (string enabledModule in enabledModuleList) {
string modulePath = moduleList.Single(m => m.Contains(enabledModule));
// using code adapted from from AssemblyLoader
var asm = AssemblyLoader.LoadAssembly(modulePath);
// register routes for module
AreaRegistrationUtil.RegisterAreasForAssemblies(asm);
// load into Ninject kernel
kernel.Load(asm);
}
}
This is the crux of the Ninject controller factory that receives the aforementioned Ninject kernel and handles requests to make controllers. For controllers that exist within an assembly in bin/Modules the GetControllerType(...) returns null for the requested controller name.
public class NinjectControllerFactory : DefaultControllerFactory
{
#region Instance Variables
private IKernel _Kernel;
#endregion
#region Constructors
public NinjectControllerFactory(IKernel kernel)
{
_Kernel = kernel;
}
protected override Type GetControllerType(System.Web.Routing.RequestContext requestContext, string controllerName)
{
// Is null for controller names requested outside of bin directory.
var type = base.GetControllerType(requestContext, controllerName);
return type;
}
protected override IController GetControllerInstance(System.Web.Routing.RequestContext requestContext, Type controllerType)
{
IController controller = null;
if (controllerType != null)
controller = _Kernel.Get(controllerType) as IController;
return controller;
}
}
Update on Ninject Nuget Install
I couldn't get it to install Ninject.MVC3 via NuGet for some reason. Visual Studio was giving some schemaVersion error when clicking the install button (I have installed other Nuget packages like ELMAH btw).
I did find out something else that was interesting though, and that is that if I pass in the extra module assembilies to the NinjectControllerFactory I have and search those when the type cannot be resolved it finds the correct type and is able to build the controller. This leads to another strange problem.
The first route to be requested from an external module is the /Account/LogOn in the auth and registration module. The virtual path provider throws an error here after it has located the view and attempts to render it out complaining of a missing namespace. This causes an error route to fire off which is handled by an ErrorHandling module. Strangely enough, this loads and render fine!
So I am still stuck with two issues;
1) Having to do a bit of a dodgy hack and pass in the extra module assemblies to the NinjectControllerFactory in order to be able to resolve types for Controllers in external modules
2) An error with one particular module where it complains about a namespace not being found
These two issues are obviously connected because the assembly loading just isn't loading up and making everything available that needs to be. If all these mvc areas are loaded from the bin directory everything works fine. So it is clearly a namespacing/assembly load issue.
LoadFrom load the assembly into the loading context. These types are not available to the other classes in the default Load context. Probably this is the reason why the controller is not found.
If you know which assemblies have to be loaded then you should always use Assembly.Load(). If you don't know which assemblies are depolyed in the directory then either guess from the filesnames the assembly names or use Assembly.ReflectionOnlyLoadFrom() (preferably using a temporary AppDomain) to get the assembly names. Then load the assemblies using Assembly.Load() with the assembly name.
If your assemblies contain NinjectModules you can also use kernel.Load() which does what I described above. But it only loads assemblies containing at least one module.
Read up http://msdn.microsoft.com/en-us/library/dd153782.aspx about the different assembly contexts.
Here is a small extract from the Ninject codebase. I removed the unnecessary stuff but did not try to compile or run so probably there are minor issues with this.
public class AssemblyLoader
{
public void LoadAssemblies(IEnumerable<string> filenames)
{
GetAssemblyNames(filenames).Select(name => Assembly.Load(name));
}
private static IEnumerable<AssemblyName> GetAssemblyNames(IEnumerable<string> filenames)
{
var temporaryDomain = CreateTemporaryAppDomain();
try
{
var assemblyNameRetriever = (AssemblyNameRetriever)temporaryDomain.CreateInstanceAndUnwrap(typeof(AssemblyNameRetriever).Assembly.FullName, typeof(AssemblyNameRetriever).FullName);
return assemblyNameRetriever.GetAssemblyNames(filenames.ToArray());
}
finally
{
AppDomain.Unload(temporaryDomain);
}
}
private static AppDomain CreateTemporaryAppDomain()
{
return AppDomain.CreateDomain(
"AssemblyNameEvaluation",
AppDomain.CurrentDomain.Evidence,
AppDomain.CurrentDomain.SetupInformation);
}
private class AssemblyNameRetriever : MarshalByRefObject
{
public IEnumerable<AssemblyName> GetAssemblyNames(IEnumerable<string> filenames)
{
var result = new List<AssemblyName>();
foreach(var filename in filenames)
{
Assembly assembly;
try
{
assembly = Assembly.LoadFrom(filename);
}
catch (BadImageFormatException)
{
// Ignore native assemblies
continue;
}
result.Add(assembly.GetName(false));
}
return result;
}
}
}
I have my target language in Session["lang"], which is either "en" or "it". I have added this to the Site.master:
<script runat="server">
void Page_Load(object sender, EventArgs e) {
string lang = Session["lang"].ToString();
System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.CreateSpecificCulture(lang);
System.Threading.Thread.CurrentThread.CurrentUICulture = System.Globalization.CultureInfo.CreateSpecificCulture(lang);
}
</script>
Then I'd like to invoke a resource string like this:
<asp:Label ID="Label1" runat="server" Text="<%$ Resources:Global, test %>"></asp:Label>
I have two files in the App_GlobalResources, named Global.resx and Global.en.resx.
The problems is that no matter what is in the lang variable, I always get the results from the main Global.resx, and I never get the english version from Global.en.resx
I am doing this wrong entirely??
I tried putting the System.Threading... part in the Application_PreRequestHandlerExecute method in Global.asax.cs but the result was the same.
Thanks
PS: I am asking about a way to make this work in a simple way. If I was to use the complicate way, I'd go with this: http://helios.ca/2009/05/27/aspnet-mvc-and-localization/
i had the same dilema(how to implement localization) in my asp.net mvc app.
I followed the instructions posted here and it works like a charm.
So i created a folder named Localization under Content and then i create Resources resx files for each language i want to translate. Keep in mind that there is a convention for the resx file names. ie
Resources.resx is the default fall back for everything.
Resources.en-GB.resx is for english GB
Resources.en-US.resx is for english US
etc.
Just make sure you follow the instructions posted in the link to embed and make the Resources available in all places in your app (views, controllers etc)
Edit:
I want to add that i ommited this line from web.config since i wanted to manually set the local from my app.
<globalization uiCulture="auto" culture="auto"/>
Instead i have created the following class:
public class SmartController : Controller
{
public SmartController()
{
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-US");
System.Threading.Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
}
}
All controllers inherit from this class.
Since this is an administrative set of the locale i have to set it from my apps settings. You could read it from Cookies and set it, or otherwise. This is imo the simplest solution for localization that i have encountered so far.
Once implemented you can refer to any string you add by the following simple line of code, no extra code needed.
<%= Resources.Strings.TranslatedTerm %>
I bet this one is a duplicate.
Anyway - all you need is here (assuming that you are using webforms viewengine (might work with others too, haven't investigated)).
Oh well... here goes my 'summary':
Helpers are just a part. You need to do some modifications with your default view engine too . On createview/createpartialview it should return localizationwebformview which adds a path key to viewdata which is used by htmlhelper to find resourceexpressionsfields and pass them to localizationhelpers class which retrieves desired value.
Little bonus=>
This might be handy if you don't want to recreate resource folders for view subfolders
(in case you modify viewengine.view/partialviewlocationformats):
private static string ReformatVirtualPath(string virtualPath)
{
//This allows NOT to duplicate App_localResources directory
// ~/Views/Shared/Partial/Some/BulltihS/_View.ascx
// turns into =>
// ~/Views/Shared/_View.ascx
var start = #"(~(/?\w*/?){2})";
var end = #"(\w*.as(c|p)x)";
start = Regex.Match(virtualPath, start).Value;
end = Regex.Match(virtualPath, end).Value;
return start + end;
}
usage:
internal static ResourceExpressionFields GetResourceFields
(string expression, string virtualPath)
{
virtualPath = ReformatVirtualPath(virtualPath);
var context = new ExpressionBuilderContext(virtualPath);
var builder = new ResourceExpressionBuilder();
return (ResourceExpressionFields)
builder.ParseExpression(expression, typeof(string), context);
}
EDIT:
but it might be a good idea to avoid App_GlobalResources and App_LocalResources as K. Scott Allen suggests (check Konstantinos answer).