Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Extra level in resx fallback system

Tags:

c#

.net

resx

In my current project we use resx files to have our application translated.
Now a request to brand our website requires for some strings to have slightly different translations.
I was thinking of extending Culture to BrandedCulture and then write a ResourceManager that uses the following fallback strategy:

  1. Resx file for requeste culture and requested brand
  2. Resx file for requested culture
  3. Resx file for fallback culture

So basically add one extra check on top of the existing one.

I wanted to create a custom IResourceProvider that basically wraps the default ResourceProvider and use that one starting from step 2 (the normal behaviour), but I'm having difficulty instantiating/inheriting the default resource provider as it is internal.

Any suggestions? Am I going down the right path?

Might look duplicate of this, but isn't

like image 251
Boris Callens Avatar asked Feb 20 '14 13:02

Boris Callens


People also ask

What is Resx extension?

The . resx resource file format consists of XML entries that specify objects and strings inside XML tags. One advantage of a . resx file is that when opened with a text editor (such as Notepad) it can be written to, parsed, and manipulated.

How to Add data in ResX file c#?

Call the ResXResourceWriter. AddResource method for each resource you want to add to the file. Use the overloads of this method to add string, object, and binary (byte array) data. If the resource is an object, it must be serializable.

How are ResX files generated?

Resource files (. resx) are only generated with source code when building a Project in Developer Studio.


1 Answers

TL;DR Use a custom resource provider to customize resource access. Use the right tools to request resources, based on your view engine in order for the resource provider factory to work.

Custom resource provider factories

Since you are asking about a web project, you can implement your own ResourceProviderFactory and define it inside your web.config file:

<system.web>
    <globalization resourceProviderFactoryType="..." />
</system.web>

The above snippet should be self-descriptive. This factory may return your custom resource provider implementation. You can also activate the standard resource provider for the default behaviour using reflection.

public override IResourceProvider CreateGlobalResourceProvider(string classKey)
{
    var factory = (ResourceProviderFactory)Activator.CreateInstance(Type.GetType("System.Web.Compilation.ResXResourceProviderFactory, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"));
    var provider = factory.CreateGlobalResourceProvider(classKey);

    return new CustomResourceProvider(provider);
}

CustomResourceProvider holds an reference of the standard resource provider to implement the default behaviour. An detailled example of writing custom providers can be found here.

However, I do not think that creating new cultures for a small subset of localized strings is a good idea. Cultures describe a certain locale and rules for formatting currencies, dates, numbers and so on. Available locales are standardised unter various ISO standards (639, 3166 and 15924). Your solution defines new cultures to model a subset of strings for an existing culture. That's not what cultures are defined for.

For example one of your strings that must be changed is in english. After it's "branding" it's still in english. Therefor the actual culture should stay the same.

You can challange this problem in different ways. Using a custom resource provider can be indeed a valid solution! By overwriting it's GetObject-method and checking for a defined prefix, for example:

public override object GetObject(string resourceKey, CultureInfo culture)
{
    if (m_brandedMode)
        return m_provider("Branded" + resourceKey, culture) ?? m_provider(resourceKey, culture);
    else
        return m_provider(resourceKey, culture);
}

Here m_brandedMode is a simple boolean switch that may be set from the resource provider factory above. If it's enabled the custom resource provider queries for the string with a "Branded"-prefix. If there's no string matching, it uses the default implementation.

Inside your resource file for english you would have two versions strings that differ based on the branding: MyString and BrandedMyString. They both share the same culture, but they are queried by the resource provider based on an application setting.

Of course this is just an example. You can also implement different data sources for branded and unbranded strings, even if this might be a little more work to do. However, I suggest not touching the cultures and also not touching the default behaviour. Resource management is sometimes a little bit tricky in .NET.

Using resources (the right way)

Based on your view engine there you have to change the way of accessing localized strings in order for the environment to pick up your custom factory. Note that I only tested this for web views with ASPX and Razor view engines, but I think this can be similarly adopted to offline applications with WinForms and WPF.

Typically if you add default resources to your project, a designer generated wrapper around it get's generated. If your default resource file is called Resources, the designer generates a class Resources within the Resources.Designer.cs file. This is pretty handy but misleading since it apparently is not intented to be used as localization source, but for symbols, icons and all this other data want to embed into your assembly. So the first step to take is decide how you want to localize your project. This can typically be done by view or globally.

Localizing by view means nothing more than you put a resource file for each view (with the same name, something like Index.cshtml.resx) inside the directory your view file is located. Globally lobalizing means that all strings for one culture go into one resource file that is typically located inside the App_GlobalResources directory. However this is not mandatory. In fact the resource file can be embedded into your assembly, another assembly or located somewhere else.

Localizing ASPX views

I am starting with localized strings for ASPX, because it's actually easier (which surprised me). In ASPX you are using the <%$ expression builder syntax to access resources:

<asp:Label runat="server" Text="<%$ Resources:MyResource, Test %>" />

This tells the compiler to use the resource Test from the MyResource resource container (by default an resource file). The expression results into a call to CreateGlobalResourceProvider("MyResource"). The returned provider get's a call to GetObject("Test").

I did not dig deeper into this, but apparently it is not directly possible to localize ASPX views locally. However, this should not be to hard and self-speaking. (The resulting call to the factory goes to GetLocalResourceProvider instead of GetGlobalResourceProvider)

Localizing Razor views

The tricky thing is that you typically use the auto-generated Resource class like so:

@MyResource.Test

This bypasses the resource provider generation by using the standard resource manager. The correct way to access localized strings is to use HttpContext:

@HttpContext.GetGlobalResourceObject("MyResource", "Test")

Gotcha! This calls GetGlobalResourceProvider("MyResource") on the custom resource provider factory. The rest is just like described previously. In a similar way you can use @HttpContext.GetLocalResourceObject("~/Views/Home/Index.cshtml", "Test") in order to request a local resource.

I have no idea why this is not a standard extension, but you can wrap this inside fancy helper methods:

public static string Resource(this HtmlHelper helper, string resourceName, string resourceKey)
{
    return helper.ViewContext.HttpContext.GetGlobalResourceObject(resourceName, resourceKey).ToString();
}

public static string Resource(this WebViewPage page, string resourceKey)
{
    return page.ViewContext.HttpContext.GetLocalResourceObject(page.VirtualPath, resourceKey).ToString();
}

The following commands will either request global or local strings:

@Html.Resource("MyResources", "Test");    // Global resource "Test" from "MyResources" container.
@this.Resource("Test");                   // Local resource "Test".

Tricky, but it should do the job!

like image 159
Carsten Avatar answered Oct 21 '22 00:10

Carsten