Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

A Localization Handler to serve ASP.NET Resources to JavaScript


:P
On this page:

I’m back working on a localization project and so I’ve been spending a lot of time on my resource provider in my ample spare time adding a few features that are required for smooth localization of an application. I talked about this in a lot of detail in my Data Drive Resource Provider article a couple of years back. This article address the server side of localization.

However, that article didn’t address the client side and so there are no real provisions for client side localization. In the samples and the localization admin form specifically I relied on ASP.NET script tag embedding  using <%= ResC("MyResourceString" %> to embed resource strings where ResC() embeds a properly encoded string into the page when the ASPX page renders. There are lots of problems with this approach including the nastiness that results from script tags in code, the fact that script tags don’t always work even in ASPX pages (specifically the good old The Controls Collection cannot be modified error), and maybe most importantly that it doesn’t work at all if you want to externalize your script into a separate .js file as <% %> tags only work in ASP.NET pages.

ASP.NET AJAX includes some localization support via ScriptManager, but personally I don’t like this approach because it works separately from the server side localization features already built into ASP.NET. You’re also bound to ScriptManager which I don’t use. With script manager you have to create localized .js files which then get loaded for the given locale which frankly is lame. The biggest issue with this is that if you hand resources to be localized to localizers they now have to work with two separate sets of localization resources. The other issue is that if you already have tools that work with server resources you can’t use them for client resources. Personally I’d like to have all resources in one place and then be able to access them from both client and server code as needed.

Creating an ASP.NET JavaScriptResourceHandler HttpHandler

So what I want to do is fairly simple: I want to be able to maintain my resources in standards ASP.NET style resources – Resx or in my case also the DbResourceProvider based resources – and be able to feed these to the client to consume. In other words use a single mechanism to create my resources, and also be able to administer those same resource through my resource administration tool described in the article mentioned earlier. Then serve those resources on the server as well as on the client.

It turns out this is fairly straight forward to implement. What I ended up with a is an HttpHandler that serves resources as one or more JavaScript objects (one for each resource set you decide publish to the client explicitly). The object has a property for each string resource Id in the resource set with values that are normalized for the active locale. By normalized I mean that the culture has been set based on resource fallback – if a specific resource is missing the next less specific resource is used following the resource hierarchy (ie. en-Us –> en –> Invariant). In effect this is the same behavior you’d see in resources on the server assigned with meta:resource keys or explicit calls to GetLocal/GlobalResourceObject() based on the current UI culture of the request.

The process to work with this is pretty straight forward:

Create your resources as standard .Resx or DbResourceProvider resources

You can create local or global resources or re-use the same resource you are already using on the server in resx files just like you normally do.

To make life easier I tend to use my resource administration form with the DbResourceProvider (included in the download) which is more interactive and can be context sensitive from ASP.NET content pages:

ResourceLocalizationForm

This tool allows using a database resource provider to modify and add resources and then optionally lets you export them back into Resx resources (or you can use the DbResourceProvider.

This is purely optional though – any way you want to create your Resx resources works just as well.

Add the HttpHandler to Web.Config

In order to use the handler the handler has to be added to Web.Config:

<!-- IIS 7 -->
<system.webServer>
  <handlers>
    <add name="JavaScriptResourceHandler" verb="GET" path="JavascriptResourceHandler.axd" 
         type="Westwind.Globalization.JavaScriptResourceHandler,Westwind.Globalization"/>
  </handlers>
</system.webServer>

<!-- IIS 6/5/5.5 and Cassini -->
<system.web>
  <httpHandlers>
    <add name="JavaScriptResourceHandler" verb="GET" path="JavascriptResourceHandler.axd" 
         type="Westwind.Globalization.JavaScriptResourceHandler,Westwind.Globalization"/>
  </httpHandlers>
</system.web>

Add Handler Reference to Pages that require Localized JavaScript Resources

The HttpHandler is driven through querystring parameters that specify the resourceSet, the localeID, what type of resources to load from whether you’re accessing global resources and so on. There are many options, so to make accessing the handler easier there are various static methods that embed the scripts into the page automatically. The easiest two versions that take all default behaviors look like this:

protected void Page_Load(object sender, EventArgs e)
{
    JavaScriptResourceHandler.RegisterJavaScriptGlobalResources(this, "globalRes","resources");
    JavaScriptResourceHandler.RegisterJavaScriptLocalResources(this, "localRes");
}

This embeds two script references into the page each of which contains an object – globalRes and localRes respectively – that contains all of the resourceids as properties. These versions assume several defaults: They choose the current UI culture, auto-detect either the Resx or DbResourceProvider and read resources out of either of these providers. Local resources assume you want the resources for the current page and that control resources are not included (control resources are those that have a . in the name and refer to Control.Property typically). All of this can be overridden with the more specific overrides but most of the time these short versions are appropriate.

The script references generated into the page are pretty long and nasty:

<script src="JavaScriptResourceHandler.axd?ResourceSet=resources&amp;
             LocaleId=de-DE&amp;VarName=globalRes&amp;ResourceType=resdb&amp;IsGlobal=1" 
        type="text/javascript"></script>
<script src="JavaScriptResourceHandler.axd?ResourceSet=localizationadmin/localizationadmin.aspx&amp
            ;LocaleId=de-DE&amp;VarName=localRes&amp;ResourceType=resdb" 
        type="text/javascript"></script>

which is why those RegisterXXX methods exist to make things easy from within page code.

You can also use this from MVC pages for which there’s are methods that just retrieve the URL. In an MVC page you can embed Client Resources like this:

<%=    
   "<script src='" + 
Westwind.Globalization.JavaScriptResourceHandler.GetJavaScriptGlobalResourcesUrl("globalRes","Resources") +
"' type='text/javascript'></script>" %>

The result JavaScript content of those script references is JavaScript that contains an object each with the resources as properties of that object. Here’s what the globalRes reference looks like:

var globalRes = {
    CouldNotCreateNewCustomer: "Neuer Kunde konnte nicht erstellt werden",
    CouldNotLoadCustomer: "Kunde konnte nicht erstellt werden",
    CustomerSaved: "Kunde wurde gespeichert",
    NoGermanResources: "This entry contains no German Resources",
    Today: "Heute",
    Yesterday: "Gestern"
};

Notice that the cultures are normalized. I’m running in german locale (de-de) and I get the de resources from the resources except for the NoGermanResources key which doesn’t include any german resources and so falls back. In short you get the same behavior as you would expect from GetLocalResourceObject or GetGlobalResourceObject on the server.

Here’s the localRes version which is more of the same, but served in a separate handler link.

var localRes = {
    AreYouSureYouWantToRemoveValue: "Sind Sie sicher dass Sie diesen Wert l\u00F6schen wollen?",
    BackupComplete: "Der Backup f\u00FChrte erfolgreich durch",
    BackupFailed: "Der Backup konnte nicht durchgef\u00FChrt werden",
    BackupNotification: "Diese Operation macht einen Backup von der Lokalisationtabelle. \nM\u00F6chten Sie fortfahren?",
    Close: "Schliessen",
    FeatureDisabled: "Diese Funktion ist nicht vorhanden im on-line Demo ",
    InvalidFileUploaded: "Unzul\u00E4ssige Akten Format hochgeladen.",
    InvalidResourceId: "Unzul\u00E4ssige ResourceId",
    Loading: "Laden",
    LocalizationTableCreated: "Lokalisations Akte wurde erfolgreich erstellt.",
    LocalizationTableNotCreated: "Die Localizations Akte konnte nicht erstellt werden."
};

Notice that the local resources by default don’t include controls (txtName.Text, txtName.Tooltip etc.), which is a common scenario. I tend to localize pages with local resources using both control based meta:resource tags and also some fixed values for messages. JavaScript code tends to need only those messages and not the control values. If you need them however you can provide them with the more specific versions of these methods. For example to explicitly specify all the options on a local resource retrieval you can use:

JavaScriptResourceHandler.RegisterJavaScriptLocalResources(this, "localRes", 
CultureInfo.CurrentUICulture.IetfLanguageTag,
"localizationadmin/localizationadmin.aspx",
ResourceProviderTypes.AutoDetect, true);

Here you get to specify the name of the variable, the explicit culture (normalized), the name of the resource (which is the relative path without the leading /) and autodetection, as well as the option to include control resources.

Using the Embedded Controls in JavaScript Code

Once the script references have been embedded those localized JavaScript resource objects are available anywhere in client code, including in external .JS files as long as you ensure that you access resources after all scripts have loaded (use jQuery and $(document).ready( function() {}  or hook window.onload).

Using the objects is as simple as this.

    $(document).ready(function() {
        alert(globalRes.CustomerSaved);

        // local resources from admin form
        alert(localRes.BackupFailed);
    });

This should work either or inside of an ASPX page or an external script .js file.

Using the Handler and embedding resources is real easy as you can see. You do have a couple of dependencies: Westwind.Globalization.dll, Westwind.Web.dll and Westwind.Utilities.dll which provide some of the core utilities used in the resource provider. If you’re adventurous you can break out the resource handler on its own – there are only a handful of dependencies in the actual handler and resx parsing code, but I was too lazy to separate this out. I leave that excercise to you if you want just the handler and resource retrieval code.

Handler Implementation

If you want to dig a little deeper, here are a few of the implementation highlights. The HttpHandler itself is fairly straight forward, however some of the resource retrieval, especially for Resx resources is a bit of a pain. The problem is that ASP.NET doesn’t expose the ResourceProvider that is in use so there’s no way from within the native ASP.NET framework to retrieve resources for a given ResourceSet. This means that custom code is needed to access resources explicitly from their source. With the DbResourceProvider this is fairly straight forward because the resources are coming from a database and there’s a DbResourceDataManager that can return resources easily. For Resx resources the handler has to go out and query the Resx resources on disk as XML and parse out the values. Further, both mechanisms need to normalize the resources which means actually retrieving resources from up to 3 separate resourcesets as each ResourceSets contains all resources for one exact locale in a non-normalized fashion.

You can look at the full code in the accompanying download (or in the repository), but here is the core HttpHandler operation (partial code):

/// <summary>
/// Http Handler that can return ASP.NET Local and Global Resources as a JavaScript
/// object. Supports both plain Resx Resources as well as DbResourceProvider driven
/// resources.
/// 
/// Objects are generated in the form of:
/// 
/// var localRes  = {
///    BackupFailed: "Backup was not completed",
///    Loading: "Loading"
/// );
/// 
/// where the resource key becomes the property name with a string value.
/// 
/// The handler is driven through query string variables determines which resources
/// are returned:
/// 
/// ResourceSet      -  Examples: "resource" (global), "admin/somepage.aspx" "default.aspx" (local)
/// LocaleId         -  Examples: "de-de","de",""  (empty=invariant)
/// ResourceType     -  Resx,ResDb
/// IncludeControls  -  if non-blank includes control values (. in name)
/// VarName          -  name of hte variable generated - if omitted localRes or globalRes is created.
/// IsGlboalResource -  Flag required to find Resx resources on disk
/// 
/// Resources retrieved are aggregated for the locale Id (ie. de-de returns de-de,de and invariant)
/// whichever matches first.
/// </summary>
public class JavaScriptResourceHandler : IHttpHandler
{

    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        HttpRequest Request = HttpContext.Current.Request;

        string resourceSet = Request.Params["Resourceset"];
        string localeId = Request.Params["LocaleId"] ?? ""; 
        string resourceType = Request.Params["ResourceType"] ?? "Resx";   // Resx/ResDb
        bool includeControls = (Request.Params["IncludeControls"] ?? "") != "";
        string varname = Request.Params["VarName"] ?? "localRes";
        bool isGlobalResource = (Request.Params["IsGlobal"] ?? "") != "";

        string cacheKey = (resourceSet + localeId + resourceType + includeControls.ToString() + 
varname + isGlobalResource.ToString()).ToLower(); string content = HttpContext.Current.Cache[cacheKey] as string; if (content != null) this.SendTextOutput(content, "application/javascript"); // Done! // varname is embedded into script so validate to avoid script injection // it's gotta be a valid C# and valid JavaScript name Match match = Regex.Match(varname, @"^[\w|\d|_|$|@]*$"); if (match.Length < 1 || match.Groups[0].Value != varname) this.SendErrorResponse("Invalid variable name passed."); if ( string.IsNullOrEmpty(resourceSet) ) this.SendErrorResponse("Invalid ResourceSet specified."); Dictionary<string, object> resDict = null; if (resourceType.ToLower() == "resdb") { DbResourceDataManager manager = new DbResourceDataManager(); resDict = manager.GetResourceSetNormalizedForLocaleId(localeId, resourceSet)
as Dictionary<string,object>; } else // Resx Resources { DbResXConverter converter = new DbResXConverter(); // must figure out the path string resxPath = converter.FormatResourceSetPath(resourceSet,!isGlobalResource); resDict = converter.GetResXResourcesNormalizedForLocale(resxPath, localeId)
as Dictionary<string,object>; } if (!isGlobalResource && !includeControls) { // filter the list to strip out controls (anything that contains a . in the ResourceId resDict = resDict.Where(res => !res.Key.Contains('.') && res.Value is string) .ToDictionary(dict => dict.Key, dict => dict.Value); } else { // must fix up . syntax which is not a valid variable name resDict = resDict.Where( res => res.Value is string) .ToDictionary(dict => dict.Key, dict => dict.Value); } string javaScript = this.SerializeResourceDictionary(resDict,varname); // cache the text for this locale and settings HttpContext.Current.Cache[cacheKey] = javaScript; this.SendTextOutput(javaScript, "application/javascript"); } /// <summary> /// Generates the actual JavaScript object map string makes up the /// handler's result content. /// </summary> /// <param name="resxDict"></param> /// <param name="varname"></param> /// <returns></returns> private string SerializeResourceDictionary(Dictionary<string, object> resxDict, string varname) { StringBuilder sb = new StringBuilder(2048); sb.Append("var " + varname + " = {\r\n"); int anonymousIdCounter = 0; foreach (KeyValuePair<string, object> item in resxDict) { string value = item.Value as string; if (value == null) continue; // only encode string values string key = item.Key; if (string.IsNullOrEmpty(item.Key)) key = "__id" + anonymousIdCounter++.ToString(); key = key.Replace(".", "_"); if (key.Contains(" ")) key = StringUtils.ToCamelCase(key); sb.Append("\t" + key + ": "); sb.Append(WebUtils.EncodeJsString(value) ); sb.Append(",\r\n"); } sb.Append("}"); // strip off ,/r/n at end of string (if any) sb.Replace(",\r\n}","\r\n}"); sb.Append(";\r\n"); return sb.ToString(); } /// <summary> /// Returns an error response to the client. Generates a 404 error /// </summary> /// <param name="Message">Error message to display</param> private void SendErrorResponse(string Message) { if (!string.IsNullOrEmpty(Message)) Message = "Invalid Web Resource"; HttpContext Context = HttpContext.Current; Context.Response.StatusCode = 404; Context.Response.StatusDescription = Message; Context.Response.End(); } /// <summary> /// Writes text output to server using UTF-8 encoding and GZip encoding /// if supported by the client /// </summary> /// <param name="text"></param> /// <param name="useGZip"></param> /// <param name="contentType"></param> private void SendTextOutput(string text, string contentType) { byte[] Output = Encoding.UTF8.GetBytes(text); HttpResponse Response = HttpContext.Current.Response; Response.ContentType = contentType; Response.Charset = "utf-8"; // Trigger Gzip encoding and headers if supported WebUtils.GZipEncodePage(); if (!HttpContext.Current.IsDebuggingEnabled) { Response.Cache.SetCacheability(HttpCacheability.Public); Response.ExpiresAbsolute = DateTime.UtcNow.AddDays(1); Response.Cache.SetLastModified(DateTime.UtcNow); } Response.BinaryWrite(Output); Response.End(); }
}

The handler provides basic caching of the resources returned based on the full parameter list which is necessary that each of the locale combinations get cached which can be a lot of combination potentially if you have a lot of languages.The process of creating these resource lists – especially from Resx resources is fairly expensive so caching is important. The output generated is also cached in the browser (in non-debug mode anyway). It’s also GZipped if the client supports it.

Again it’s a shame we don’t get access to the native ASP.NET ResourceProvider because the providers themselves cache the resource sets internally so they typically don’t get reloaded from the underlying resource store. Alas we don’t have access to the resource provider so we either duplicate the caching or simply reload the lists. The retrieval methods here go back to the source data:

if (resourceType.ToLower() == "resdb")
{
    DbResourceDataManager manager = new DbResourceDataManager();
    resDict = manager.GetResourceSetNormalizedForLocaleId(localeId, resourceSet) 
as Dictionary<string,object>; } else // Resx Resources { DbResXConverter converter = new DbResXConverter(); // must figure out the path string resxPath = converter.FormatResourceSetPath(resourceSet,!isGlobalResource); resDict = converter.GetResXResourcesNormalizedForLocale(resxPath, localeId)
as Dictionary<string,object>; }

to retrieve resources and then try to cache the result instead using the ASP.NET Cache object to avoid the round trip to the underlying resource store.

The core task of the handler wraps the actual  resource set export routines that are handled by DbResxConverter (which is used for other things like exporting from Db <—> Resx) and the DbResourceManager respectively. Both of those functions return a dictionary of ResourceId strings and resource values. For JavaScript usage non-string objects are dropped. The actual creation of the JavaScript object then is trivial by simply looping through the dictionary as shown in SerializeResourceDictionary which creates the JavaScript object map as a string. Note that values need to be properly encoded as JavaScript code strings. For this I use a simple EncodeJs function that encodes strings properly for Javascript usage.

Resx Resource Retrieval

All of the above is pretty straight forward. The worst part of the process however is to retrieve the Resx resources and normalize them. It really bugs me that ASP.NET doesn’t allow access to the ASP.NET ResourceProvider, because the provider actually contains methods to do this for you. But alas there appears to be no way to access the ResourceProvider directly so you have to resort to retrieve resources manually (or re-instatiate the provider which is a bit of a pain).

Actually there’s an easier way than manually parsing the Xml files: System.Windows.Forms.ResXResourceReader provides access to Resx resources, but it pulls in System.Windows.Forms. It also doesn’t provide caching unless you manually cache the resource sets returned statically which in effect duplicates what the ResourceProvider already does natively. In any case I decided against using ResXResourceReader because of the System.Windows.Forms dependency. Instead I used some straight forward code to parse the Resx resources manually. This code already exists in the library for importing ResX resources into the Databased Resource provider anyway.

To manually import ResX files is pretty straight forward using XML parsing. The following two methods retrieve an individual resource set from a .Resx file and then normalize the set by retrieving all the resource sets in the chain and combining the resource dictionaries:

/// <summary>
  /// Gets a specific List of resources as a list of ResxItems.
  /// This list only retrieves items for a specific locale. No
  /// resource normalization occurs.
  /// </summary>
  /// <param name="FileName"></param>
  /// <returns></returns>
  public List<ResxItem> GetResXResources(string FileName)
  {
      string FilePath = Path.GetDirectoryName(FileName) + "\\";

      XmlDocument Dom = new XmlDocument();

      try
      {
          Dom.Load(FileName);
      }
      catch (Exception ex)
      {
          this.ErrorMessage = ex.Message;
          return null;
      }

      List<ResxItem> resxItems = new List<ResxItem>();
      
      XmlNodeList nodes = Dom.DocumentElement.SelectNodes("data");

      foreach (XmlNode Node in nodes)
      {
          string Value = null;

          XmlNodeList valueNodes = Node.SelectNodes("value");
          if (valueNodes.Count == 1)
              Value = valueNodes[0].InnerText;
          else
              Value = Node.InnerText;

          string Name = Node.Attributes["name"].Value;
          string Type = null;
          if (Node.Attributes["type"] != null)
              Type = Node.Attributes["type"].Value;

          ResxItem resxItem = new ResxItem() { Name = Name, Type = Type, Value = Value };
          resxItems.Add(resxItem);
      }
      return resxItems;
  }

  /// <summary>
  /// Returns all resources for a given locale normalized down the hierarchy for 
  /// a given resource file. The resource file should be specified without the
  /// .resx and locale identifier extensions.
  /// </summary>
  /// <param name="baseFile">The base Resource file without .resx and locale extensions</param>
  /// <param name="LocaleId"></param>
  /// <returns>Dictionary of resource keys and values</returns>
  public Dictionary<string, object> GetResXResourcesNormalizedForLocale(string baseFile, string LocaleId)
  {
      string LocaleId1 = null;
      if (LocaleId.Contains('-'))
          LocaleId1 = LocaleId.Split('-')[0];
      
      List<ResxItem> localeRes = new List<ResxItem>();
      List<ResxItem> locale1Res = new List<ResxItem>();
      List<ResxItem> invariantRes = null;

      if (!string.IsNullOrEmpty(LocaleId))
      {
          localeRes = this.GetResXResources(baseFile + "." + LocaleId + ".resx");
          if (localeRes == null)
              localeRes = new List<ResxItem>();
      }
      if (!string.IsNullOrEmpty(LocaleId1))
      {
          locale1Res = this.GetResXResources(baseFile + "." + LocaleId1 + ".resx");
          if (locale1Res == null)
              locale1Res = new List<ResxItem>();
      }

      invariantRes = this.GetResXResources(baseFile + ".resx");        
      if (invariantRes == null)
          invariantRes = new List<ResxItem>();

      IEnumerable<ResxItem> items =
              from loc in localeRes
                      .Concat(from loc1 in locale1Res select loc1)
                      .Concat(from invariant in invariantRes select invariant)
                      .OrderBy(loc => loc.Name)
              select loc;

      Dictionary<string, object> resxDict = new Dictionary<string, object>();
      string lastName = "@#XX";
      foreach (ResxItem item in items)
      {
          if (lastName == item.Name)
              continue;
          lastName = item.Name;
          
          resxDict.Add(item.Name, item.Value);
      }
      
      return resxDict;
  }

GetResXResources retrieves a list of all resources for a specific ResX file. This will be locale specific. GetResXResourcesNormalizedForLocale then figures out the resource fallback locales and retrieves those resource sets as well. Up to 3 resource sets are then merged using LINQ’s .Concat() function, ordered and filtered to produce a single normalized ResourceSet for what should be all the resources available.

LINQ made this filtering process a real breeze although I did run into a snag doing the final filtering. I couldn’t figure out how to do the final filtering to get just  the first unique Resource Id as a group.  Sometimes the old fashioned way is just quicker than farting around with trying to make LINQ work and so i fell back to an old fashioned foreach loop. <g>

The process for DbResourceProvider is similar although it’s a little easier to retrieve since the data is coming from the database and a bit easier to combine. You can check out the code in DbResourceDataManager for GetResourceSet() and GetResourceSetNormalizedForLocaleId() to see the same logic with the database provider. The idea is the same – each resourceset is retrieved and combined and filtered.

Check it out

I’ve provided updated code to my Westwind.Globalization project which is an updated version of the Localization provider article’s code. There are many improvements in this code from the article’s code.  The handler and ResourceProvider are also part of the West Wind Web Toolkit  for ASP.NET which is a separate product that’s still under construction for documentation and samples etc. That site includes source repository access for updates as well.

Download code (Westwind.Globalization only)

Download code (Westwind.WebToolkit)

Posted in ASP.NET  AJAX  JavaScript  Localization  

The Voices of Reason


 

DotNetKicks.com
April 02, 2009

# A Localization Handler to serve ASP.NET Resources to JavaScript

You've been kicked (a good thing) - Trackback from DotNetKicks.com

Yvan
April 02, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Well Done...

This is basically what I dream of since a few years but had no time to spend on!

Does it allows to mix the "JavaScript" resources with the regualr ones? (I guess so)

Does it changes in anyway the regular web form resource handling? (I guess not)



Well Done...
(A daily reader of your column)

Yordan Georgiev
April 02, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

The problem with localization in ASP.NET is the wrong starting point of the architecture.
Brave statement. Let me justify it with the following question - answer game:
- what happens if you would have to change the error message for some GUI action for the end-users ( let us assume that that some business process change triggered the whole change , now the message is totally wrong even if it was correct in the very beginning when it was coded:
Answer: the whole project would have to go to the very big process of production deployment with all the required hassles , since the project needs to recompile !!!
- what happens if you would like to add a new language to your web app:
Answer: see answer 1
- what happens if you would like to change some labels of some controls
Answer: see answer 1
- what happens if you found out (regardless of all reviews and testing ) an embarassing typo of a GUI component ( pls, do not say that never heard about something like that ... ) I 've seen typo's on all major software vendor sites
Answer: see answer 1
I am sure you know even more questions

My point is that the application layer ( the code which does need to recompile in order to work ) is the WRONG place for storing any data. Store the data in the database and make your app layer flexible enough to present and utilize this data.
Frankly said I've never seen a real working application utilizing this idea, this is why I am currently working on my own implementation of it ...

The latest app layer I have been coding is clever enough to not need recompile if some label text changes ... yet it is much more difficult it achieve the above described idea.

There is also a very human explanation of why it has happened and probably will go on in the future also -the Application layer coders do not want to do that since they will have much less employment - since the power later on goes to the sql side. You can find those silos of "my work is the most important and if my part does not work , the whole think will not work " all over the software world , which is part of the overall software crisis phenomenon, but ... that is entirely new discussion , sorry for distracting this post ; ) ...

Rick Strahl
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

@Yordan - if you check out my previous post you will find a database driven resource provider that does what you want.

The data driven resource provider is included in this example code posted.

Kevin Babcock
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Excellent post! Thanks!

Travis Illig
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

While you might be tied to the ScriptManager if you use the built-in localization, you're not required to create separate .JS files for every culture - you can get a JSON style object with the localized resources just the way you're doing in your example, too.

Embed your script and mark up your assembly with a WebResourceAttribute to allow it to serve the script via WebResource.axd...
[assembly: WebResource("MyNamespace.MyScript.js", "application/x-javascript")]


Add your .resx file to your assembly and add a ScriptResourceAttribute to your assembly to tie the .resx file to the script and give a name to the JSON object that will appear on the client...
[assembly: ScriptResource("MyNamespace.MyScript.js", "MyNamespace.ResxNamespace.FormStrings", "FormStrings")]


Then add your script reference in the ScriptManager on your ASP.NET page and set the "EnableScriptGlobalization" property to true...
<asp:ScriptManager runat="server" ID="sm" EnableScriptGlobalization="true">
  <Scripts>
    <asp:ScriptReference Assembly="MyAssembly" Name="MyNamespace.MyScript.js" />
  </Scripts>
</asp:ScriptManager>


That generates a JSON object out of the .resx file you marked with your ScriptResourceAttribute that you can access from your main script. To tie into a different resource provider, you can change the ResourceProviderFactory attached to the app to return managers that get resources from your alternate location.

Owain Cleaver
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

a/ Agree with Travis.

b/ Also agree the Asp.net Ajax approach to script resources sucks in that you can't expose Global (or local) resources. I got around this by creating an expression builder to evaluate an external assembly's resources and put all common strings in that - I tend to use explicit resource binding rather than the meta: method so this suited me.

With you local resource js provider, do you get the local resources for the page or do you iterate through all child control's local resources too, out of interest (and i'm too lazy to download code)?

Rick Strahl
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

@Travis - thanks for the clarification. My point was that you can get server Web resources stored in the assembly (or whereever) but not local or global resources? I thought this was supported but I didn't find docs on this (which is a whole other issue on ASP.NET AJAX stack). The nice thing about using global and local resources that it's one mechanism that can work for both client and server, so you only have one set of tools you need to use for localization.

@Owain, the provider pulls only the resources you specify for a given resource set. So while it only pulls page level resources not child controls or master page resources, you can easily ask for those as well in separate localization objects. You'd just have to specify the resource set name (ie. virtual path: subdir/masterpage.master for example). This isn't as clean as it could be I suppose, but it gives you access to this stuff.

Keep in mind also that the handler only returns non-control resources by default (but you can override that), so I'm not sure if control level localizations are as important for page level code. I suspect you'd only do this in control based code (ie. user control or custom control) where localization is handled internally.

Bertrand Le Roy
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

I have to admit I don't understand any of this: "it works separately from the server side localization features already built into ASP.NET. You’re also bound to ScriptManager which I don’t use. With script manager you have to create localized .js files which then get loaded for the given locale which frankly is lame. The biggest issue with this is that if you hand resources to be localized to localizers they now have to work with two separate sets of localization resources. The other issue is that if you already have tools that work with server resources you can’t use them for client resources. Personally I’d like to have all resources in one place and then be able to access them from both client and server code as needed."

In your implementation, the zipped version is not cached, which means that even if you cached the js resource, you are rezipping every time.
You are also not doing output caching, using ASP.NET cache instead, which is not optimal.
The cache key that you are using is built using direct user input so it seems like it would be trivial to flood the cache by sending random culture ids for example.

Rick Strahl
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

@Bertrand - maybe I'm missing a key feature, but I couldn't find any docs. How do you push global or local resources down to the client with ScriptManager? I see sending .js file resources and picking the proper locale, but not sending local or global resources. If you do files you're using another approach for localization which is duplication of work most likey using an alternate resource encoding scheme (code in this case vs. .resx strings).

I agree about the cacheability - thanks for the feedback. I've had problems with OutputCache not caching properly in several situations so I've fallen back to using the ASP.NET cache instead. I need to check this out again to see if I can get this to work properly. BTW, I think this process should be easier: Instead of having to specify the host of cache headers required it'd be nice if there was an easy way to specify that you want this request output cached with an expiration and a collection of cache restriction options.

As to the parameters for the key I hadn't thought of that. But that would also be a problem for output caching if you use varyByParm, right? I don't think there's a way to avoid that if you accept varied content.

Bertrand Le Roy
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

It is true that the script localization feature is aimed at component developers, not app developers, so I don't think you can use the global/local resource folders for that. There is clearly something to improve there, and/or opportunity for third parties. But I still don't understand the bits about having to create localized js files or not being able to work with existing resource editing tools.
Output caching is not as hard as it seems, and I'm not sure what you mean about having to specify the headers. Maybe I'm missing something but if you check out the code in ScriptResourceHandler for example you can see what code we're using for that and I don't remember there being a lot of header noise in there.
But it's easy to expose yourself to cache flooding issues if you're not careful. You are right that output caching doesn't provide better protection than ASP.NET caching, which is why we cryptographically hash the parameter that we vary the cache entries on, so that an attacker can't spoof them and flood the cache.
In your case, it might be enough to normalize the cache key to values that you know are available before you use it.

Rick Strahl
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

@Bertrand - understood. I think if you are a control vendor you're probably serving resources out of compiled resources directly and do so in server code. It'd probably be a good idea to extend any handler functionality to also be able to serve resources directly from a resource file rather than relying on the ASP.NET Resource Provider model. I never really understood why every UI platform in .NET seems to use its own resource model: ASP.NET, WinForms and WPF all use very different and somewhat incompatible mechanisms which is lame.

As far OutputCaching goes - I just set this up in the handler and... drum roll... it's not working. All the VaryByParam values are there, but the request just won't cache.

HttpCachePolicy cache = Response.Cache;

// OutputCache
cache.SetExpires(DateTime.UtcNow.AddMinutes(10));
cache.SetMaxAge( new TimeSpan(0,10,0) );
cache.SetCacheability(HttpCacheability.Server);
cache.SetValidUntilExpires(true);

cache.VaryByParams["LocaleId"] = true;
cache.VaryByParams["ResoureType"] = true;
cache.VaryByParams["IncludeControls"] = true;
cache.VaryByParams["VarName"] = true;
cache.VaryByParams["IsGlobal"] = true;
cache.VaryByContentEncodings["gzip"] = true;            


But there's no chaching. Repeated hits just keep refiring the handler. I've mucked around with VaryByParams including *, no difference. I've always had issues with OutputCache in handlers and there's really so little you can do to figure what's wrong, since there's no way to check the cache or get any feedback whether it was added or if it wasn't on why adding failed.

Kevin Pirkl
April 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

I did this quick video a while back with a quick POC on JavaScript delivery of localized text from .Net Satellite assembly resource files. No where near as cool as your work here but JavaScript delivery of localized text is a cool and innovative way to do it and separates localization efforts into a different logical layer. It's not the best video but the concept is cool http://software.intel.com/en-us/videos/zero-code-aspnet-localization-via-jquery-json/

Very nice work Rick... I wish I was not doing so much PHP coding of late..

Cheers.

james westgate
April 06, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Hey Rick,

I blogged about this recently, using jQuery to retrieve the contents of a local or global .resx file using an http handler. Lightweight solution that might be of use to people who don't want to go down the MS Ajax route.

http://bloggingdotnet.blogspot.com/2009/02/javascript-localization-using-net.html

Pablo Cibraro (Cibrax)
April 08, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Hi Rick,

here you have a version of your handler that supports conditional gets with etags for caching. It works great for IE and Firefox, I haven't tried out with other browsers. The etags are just autogenerated guids, this could be improved with a real hash of the content.

public class CacheEntry
    {
        public string Etag { get; set; }
        public string Content { get; set; }
    }
    
    public class JavaScriptResourceHandler : IHttpHandler
    {

        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            HttpRequest Request = HttpContext.Current.Request;

            string resourceSet = Request.Params["Resourceset"];
            string localeId = Request.Params["LocaleId"] ?? ""; 
            string resourceType = Request.Params["ResourceType"] ?? "Resx";   // Resx/ResDb
            bool includeControls = (Request.Params["IncludeControls"] ?? "") != "";
            string varname = Request.Params["VarName"] ?? "localRes";
            bool isGlobalResource = (Request.Params["IsGlobal"] ?? "") != "";
            string etag = Request.Headers["If-None-Match"];
            
            string cacheKey = (resourceSet + localeId + resourceType + includeControls.ToString() + varname + isGlobalResource.ToString()).ToLower();
            
            CacheEntry entry = HttpContext.Current.Cache[cacheKey] as CacheEntry;
            if (entry != null)
            {
                if(etag == entry.Etag)
                {
                    this.SendNotModifiedResponse();
                }
                else
                {
                    string content = entry.Content;
                    this.SendTextOutput(content, "application/javascript", entry.Etag);  // Done!
                }
            }

            // varname is embedded into script so validate to avoid script injection
            // it's gotta be a valid C# and valid JavaScript name
            Match match = Regex.Match(varname, @"^[\w|\d|_|$|@]*$");            
            if (match.Length < 1 || match.Groups[0].Value != varname)
                this.SendErrorResponse("Invalid variable name passed.");

            if ( string.IsNullOrEmpty(resourceSet) )
                this.SendErrorResponse("Invalid ResourceSet specified.");

            Dictionary<string, object> resDict = null;
            if (resourceType.ToLower() == "resdb")
            {
                DbResourceDataManager manager = new DbResourceDataManager();
                resDict = manager.GetResourceSetNormalizedForLocaleId(localeId, resourceSet) as Dictionary<string,object>;                
            }
            else  // Resx Resources
            {
                DbResXConverter converter = new DbResXConverter();
                // must figure out the path
                string resxPath  = converter.FormatResourceSetPath(resourceSet,!isGlobalResource);
                resDict = converter.GetResXResourcesNormalizedForLocale(resxPath, localeId) as Dictionary<string,object>;
            }
            
            if (!isGlobalResource && !includeControls)
            {
                // filter the list to strip out controls (anything that contains a . in the ResourceId
                resDict = resDict.Where(res => !res.Key.Contains('.') && res.Value is string)
                                 .ToDictionary(dict => dict.Key, dict => dict.Value);
            }
            else
            {
                // must fix up . syntax which is not a valid variable name
                resDict = resDict.Where( res => res.Value is string)
                           .ToDictionary(dict => dict.Key, dict => dict.Value);
            }

            string javaScript = this.SerializeResourceDictionary(resDict,varname);

            etag = Guid.NewGuid().ToString();
            // cache the text for this locale and settings
            HttpContext.Current.Cache[cacheKey] = new CacheEntry { Content = javaScript, Etag = etag };

            this.SendTextOutput(javaScript, "application/javascript", etag);
        }

        private string SerializeResourceDictionary(Dictionary<string, object> resxDict, string varname)
        {
            StringBuilder sb = new StringBuilder(2048);

            sb.Append("var " + varname + " = {\r\n");

            int anonymousIdCounter = 0;
            foreach (KeyValuePair<string, object> item in resxDict)
            {
                string value = item.Value as string;
                if (value == null)
                    continue; // only encode string values

                string key = item.Key;
                if (string.IsNullOrEmpty(item.Key))                
                    key = "__id" + anonymousIdCounter++.ToString();                                    

                key = key.Replace(".", "_");
                if (key.Contains(" "))
                    key = StringUtils.ToCamelCase(key);

                sb.Append("\t" + key + ": ");
                sb.Append(WebUtils.EncodeJsString(value) );
                sb.Append(",\r\n");
            }
            
            sb.Append("}");

            // strip off ,/r/n at end of string (if any)
            sb.Replace(",\r\n}","\r\n}");
            sb.Append(";\r\n");

            return sb.ToString();
        }

        /// <summary>
        /// </summary>
        private void SendNotModifiedResponse()
        {
            HttpContext Context = HttpContext.Current;

            Context.Response.StatusCode = (int)HttpStatusCode.NotModified;
            Context.Response.End();
        }


        private void SendErrorResponse(string Message)
        {
            if (!string.IsNullOrEmpty(Message))
                Message = "Invalid Web Resource";

            HttpContext Context = HttpContext.Current;

            Context.Response.StatusCode = 404;
            Context.Response.StatusDescription = Message;
            Context.Response.End();
        }

        private void SendTextOutput(string text, string contentType, string etag)
        {
            byte[] Output = Encoding.UTF8.GetBytes(text);

            HttpResponse Response = HttpContext.Current.Response;
            Response.ContentType = contentType;
            Response.Charset = "utf-8";

            // Trigger Gzip encoding and headers if supported
            WebUtils.GZipEncodePage();

            HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.Public);
            HttpContext.Current.Response.Cache.SetExpires(DateTime.Now.Add(TimeSpan.FromDays(30)));
            HttpContext.Current.Response.Cache.SetMaxAge(TimeSpan.FromDays(30));
            HttpContext.Current.Response.Cache.SetSlidingExpiration(true);
            HttpContext.Current.Response.Cache.SetETag(etag);

            Response.BinaryWrite(Output);
            Response.End();
        }

        public static void RegisterJavaScriptGlobalResources(Control control, string varName, string resourceSet, string localeId, 
                                                           ResourceProviderTypes resourceType)
        {
            
            if (resourceType == ResourceProviderTypes.AutoDetect)
            {
                if (DbSimpleResourceProvider.ProviderLoaded || DbResourceProvider.ProviderLoaded)
                    resourceType = ResourceProviderTypes.DbResourceProvider;
            }

            
            StringBuilder sb = new StringBuilder(512);

            sb.Append("JavaScriptResourceHandler.axd?");
            sb.AppendFormat("ResourceSet={0}&LocaleId={1}&VarName={2}&ResourceType={3}",
                             resourceSet,localeId,varName,
                             resourceType == ResourceProviderTypes.DbResourceProvider ? "resdb" : "resx");
            sb.Append("&IsGlobal=1");

            ClientScriptProxy.Current.RegisterClientScriptInclude(control,typeof(JavaScriptResourceHandler),
                                                                  sb.ToString(),ScriptRenderModes.Script);             
        }

        public static void RegisterJavaScriptGlobalResources(Control control, string varName, string resourceSet, string localeId)
        {
            ResourceProviderTypes type = ResourceProviderTypes.Resx;
            if (DbSimpleResourceProvider.ProviderLoaded || DbResourceProvider.ProviderLoaded)
                type = ResourceProviderTypes.DbResourceProvider;

            RegisterJavaScriptGlobalResources(control, varName, resourceSet, localeId, type);
        }

        public static void RegisterJavaScriptGlobalResources(Control control, string varName,  string resourceSet)
        {

            ResourceProviderTypes type = ResourceProviderTypes.Resx;
            if (DbSimpleResourceProvider.ProviderLoaded || DbResourceProvider.ProviderLoaded)
                type = ResourceProviderTypes.DbResourceProvider;

            RegisterJavaScriptGlobalResources(control, varName, resourceSet, CultureInfo.CurrentUICulture.IetfLanguageTag, 
                                              type);
        }

        public static void RegisterJavaScriptLocalResources(Control control, string varName, string localeId, string resourceSet,
                                                            ResourceProviderTypes resourceType, bool includeControls)
        {

            if (resourceType == ResourceProviderTypes.AutoDetect)
            {
                if (DbSimpleResourceProvider.ProviderLoaded || DbResourceProvider.ProviderLoaded)
                    resourceType = ResourceProviderTypes.DbResourceProvider;
            }

            StringBuilder sb = new StringBuilder(512);

            sb.Append("JavaScriptResourceHandler.axd?");
            sb.AppendFormat("ResourceSet={0}&LocaleId={1}&VarName={2}&ResourceType={3}",
                resourceSet, localeId, varName, resourceType == ResourceProviderTypes.DbResourceProvider ? "resdb" : "resx" );
            if (includeControls)
                sb.Append("&IncludeControls=1");
           
            ClientScriptProxy.Current.RegisterClientScriptInclude(control, typeof(JavaScriptResourceHandler),
                                                                  sb.ToString(), ScriptRenderModes.Script);
        }

        public static void RegisterJavaScriptLocalResources(Control control, string varName, string localeId)
        {
            ResourceProviderTypes type = ResourceProviderTypes.Resx;
            if (DbSimpleResourceProvider.ProviderLoaded || DbResourceProvider.ProviderLoaded)
                type = ResourceProviderTypes.DbResourceProvider;

            // get the current page path as a app relative path
            string resourceSet = WebUtils.GetAppRelativePath();

            RegisterJavaScriptLocalResources(control, varName, localeId, resourceSet, type, false);
        }

        public static void RegisterJavaScriptLocalResources(Control control, string varName)
        {
            ResourceProviderTypes type = ResourceProviderTypes.Resx;
            if (DbSimpleResourceProvider.ProviderLoaded || DbResourceProvider.ProviderLoaded)
                type = ResourceProviderTypes.DbResourceProvider;

            // translate current page path into resource path
            string resourceSet = WebUtils.GetAppRelativePath();

            RegisterJavaScriptLocalResources(control, varName, CultureInfo.CurrentUICulture.IetfLanguageTag, resourceSet, type, false);
        }
    }

Dave Tigweld
April 08, 2009

# Resources on Demand

I saw the title of your article with some great excitement.
However, I was a little disappointed as it looks like you are prefetching the localized strings that you "Might Need". My app has over 10,000 localized strings.
What I was hoping to see was that you were in javascript and were fetching the localized strings as needed from the server by using an AJAX framework.

alert(fetchLocalizedString(1212,1033));

function fetchLocalizedString(resourceIdToFetch,locale)
{
//check local cache to see if we have fetched this yet
//code here to make call to server and return results if we have not already cached this.
//save result to local cache to avoid further roundtrips for same string id
}

Pablo Cibraro
April 08, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Dave, if all your localized strings are located in a single global resource, this approach is not going to scale at all. Now, if you split your localized strings in different local resources (Every page has only one local resource per culture), it should work, you will only download the strings required for the rendered page.

Rick Strahl
April 08, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

@Dave - I thought about that but didn't go down this road because in most situations it will be quicker and less server intensive to download all resources that a page might need rather than making a million individual calls.

However, it'd be easy enough to modify the handler to return just a single resource as well - but I think that's a bad call. If you have 10,000 resources and they're all in one file you probably have some rethinking to do about how to structure your localization layout.

Rick Strahl
April 08, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

@Pablo - thanks for the feedback (got your email too). It turns out the problem was the use of Response.End() in the handler which prevented the items from going into the cache. ETag shouldn't be necessary for Output Cache.

Luster Tanks
April 09, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

ASP.NET Cache being stand-alone & InProc is the real problem. Trying to "simulate" distribution through SqlCacheDependency is really a "hack" in my opinion. The right way to solve this scalability problem is through an in-memory distributed cache.

And, Microsoft is finally realizing it as well. They're working on Velocity but that is still in its infancy and will take some time to stabalize and mature.

I personally like NCache which is really impressive because of its rich set of caching topologies (Mirrored, Replicated, Partitioned, Partition-Replica, and Client Cache). The cool thing is that it also has NCache Express which is free for 2-server environments.

Oscar Calvo
April 16, 2009

# Mixed mode

I have a mixed mode to localize javascript files, I´m testing it yet it, but I think works fine.
I have a web project with one file for global resources named "ClientSide.resx" (and clientSide.es.resx etc).

Then, I add a new class library project and I add a file named "clientSide.js" without text.I also add like "existing item" the file "ClienteSide.resx" (and ClientSide.es.resx") but the trick is I add it like "link".
Ok, I put the code needed in assenbly.info and I put the reference in my Masterpage.Then I have a class with the resx like json for call from all my javascript files.
Its easy and workd fine.

mayank
June 01, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

hi all ,


i just wnat to use alert method by using java script in asp.net accrding to resources can any one tell in simple words how can i do this

Scott
June 14, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

This is an interesting approach. I was searching for something similar, and ending up with a different solution that works for me. I wrote about it here:

http://stackoverflow.com/questions/987594/localize-javascript-messages-and-validation-text/988362#988362

Yauhen
June 25, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Issue with norwegian languages.
They have 2 cultures nn-NO, nb-NO.

Default asp.net server localization could find Resources.no.resx for both cultures. Localization handler can't. I have found following trick in msft code when I have used reflector:

  ResourceSet set = this.InternalGetResourceSet(culture, true, true);
    if (set != null)
    {
        string str = set.GetString(name, this._ignoreCase);
        if (str != null)
        {
            return str;
        }
    }
    ResourceSet set2 = null;
    while (!culture.Equals(CultureInfo.InvariantCulture) && !culture.Equals(this._neutralResourcesCulture))
    {
        culture = culture.Parent;
        set = this.InternalGetResourceSet(culture, true, true);



So they try to use parent cultures when they can't find in current culture.
Such logic guarantee that users with both nn-NO, nb-NO will see .no.resx resources. Client hanler in this case loads invariant culture :(

Rick Strahl
June 25, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Sorry Yauhen, I'm not sure I understand. Why should it find this if the cultures are really nn-NO and nb-NO? There's no common ancestor in this language pair?

Yauhen
June 26, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

nn-NO and nb-NO cultures both have parent culture no. I can't say that your logic is incorrect but msfts logic try to use this parent (ancestor) culture when resolving resouvre file (if there are no specific culture files). but your logic doesn't try to use ancestor.

Yordan Georgiev
July 08, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

I had to come back because of my above statement. I have to admit that I am not familiar with your handler. I surely does provide the type of functionalities is supposed to.

I was talking about a way of localizing the whole app ( javascripts , utility classes , presentation layer , dll's - everyting) - e.g. all the messages are stored in the database . and later one languages and translations or updates to existing msgs are added to the database without need to recompiling.

I did finnish the demo providing this functionality.

The way of getting the messages is as follows:
//get the msg repo
MyApp.Dh.RepoGlobalMsgs r = MyApp.Config.AppSettings.Instance.RepoGlobalMsgs;
int langInfo = userObj.UserSettings.GuiLanguageId ;
//and you use it as is
throw new Exception(r.GetMsg(EnuMsg.ErrorForControlFormattingUnknownControlType, langInfo).MsgTxt);

I quess the code is self explanatory.

The only drawback I can think of is the fact that all the global msgs for the application should be created when instantiating the AppSettings Singleton .... but from the server point of view is it not the same whether the text will be in the file system (dlls , localization resources ) , since it will be stored in RAM anyway.

The reason why I did chose this solution is the strong believe that everything which is updatable data ( even metadata for those data ) should be stored in db. The responsibility for the app is to know how to retrieve those data and show it.

I am able to show the code if someone is interested ...

Joel
September 03, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

great work, how can i use multiple global resource file? i tried doing this but only the last registered globalRes file values can be accessed, previous resx file values are undefined

Joel
September 07, 2009

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

may i ask how to use this for precompiled application?

Todd
September 02, 2010

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Rick,

Nice approach to solving the localization problem when not wanting to duplicate your efforts for server side and client side. I'm using ASP.NET MVC, though, and am getting a 404 error when I reference the HTTPHandler from my script tag - I think this is because MVC is trying to route the request. The default ignore route is:

            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");


and my script tag looks like this:

    <script src="<%=Url.Content(string.Format("~/JavaScriptResourceHandler.axd?ResourceSet={0}&amp;LocaleId={1}&amp;VarName={2}&amp;ResourceType={3},IsGlobal={4}", 
        "resource", System.Globalization.CultureInfo.CurrentCulture.IetfLanguageTag, "globalRes", "resx", "1"))%>"
        type="text/javascript" language="javascript"></script>


is implementing IHTTPHandler the right implementation for MVC or should we be looking at using an action that returns a FileResult?

Thanks,
Todd

Gaurav
November 30, 2011

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Hi Rick,

Thanks a lot for great work!!
I am working on asp.net 3.5 language globalization project and using your tool.

I am getting "Index out of range exception" in LocalizationAdmin.aspx.cs
at below mentioned:

if (this.lstResourceSet.Items.Count > 0)
{
this.ResourceSet = this.lstResourceSet.Items[1].Value;

please help!

Thanks again.

Ilyas
September 12, 2013

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Hi Rick,
thanks for your great work.
Is it possible to access resx files stored in an external assembly?

Thanks,
Ilyas

Coseta
April 15, 2014

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Hi, Rick! If you're involved in software localization projects, you should consider checking out this online localization tool: https://poeditor.com/
It’s a very intuitive online translation platform; it is perfect for collaborative translation and has great crowdsourcing potential.
Cheers and good luck with your projects!

chris
February 24, 2015

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

Hey Rick, I really like your work. I'm trying to implement the javascriptresourcehandler using classic mode. But this doesn't work. The requests are answered with a PlatformNotSupportedException, telling me that the integrated pipelinemode has to be used. I followed the implementation-guide in your post, but I was only able to make it work using integrated mode and adding the handler to the handler-section (not the httphandler-section). Is the classic mode not supported anymore?

Regards
Christoph

Rick Strahl
February 24, 2015

# re: A Localization Handler to serve ASP.NET Resources to JavaScript

@Chris - Not sure. I think that should work, but you'll have to make sure the handlers are registered in the <system.web> section instead of <system.webServer>. I think you may have the configuration backwards? the httpHandler section is classic mode, handler is integrated.

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024