HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpModerate

CultureInfo with fallback routing to another language

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
withcultureinfolanguagefallbackanotherrouting

Problem

Our company needs a localization/translation behavior which allows incomplete (ResX) resources.
If a String

  • isn't available in italian



  • fall back to the next roman language, like french



  • fall back to our invariant (in this case: german)



The easiest approach was a custom CultureInfo.

```
///
/// A which switches to another language instead of .
///
public class FallbackCultureInfo : CultureInfo
{
private static readonly List CultureInfos = new List();
private readonly CultureInfo fallback;
private CultureInfo determinedParent;

public override CultureInfo Parent
{
get
{
if (this.determinedParent != null)
{
return this.determinedParent;
}

var originalParent = base.Parent;
if (Object.Equals(originalParent, CultureInfo.InvariantCulture) && (this.fallback != null))
{
return this.determinedParent = this.fallback;
}

if (this.fallback == null)
{
return this.determinedParent = originalParent;
}

this.determinedParent = FallbackCultureInfo.Build(originalParent.Name, this.fallback.Name);
return this.determinedParent;
}
}

private FallbackCultureInfo(String name, CultureInfo fallback = null) : base(name)
{
this.fallback = fallback;
}

///
/// Builds a with a custom fallback behavior, which switsches to
/// another language before it gets .
///
///
/// CultureInfo.CurrentUICulture = FallbackCultureInfo.Build("it-CH", "fr-CH", "de-CH");
///
///
/// Due to a missing .operator== we have to ensure a unique instance per name on our own.
///
/// Name of our culture, like "en-US"
/// Fallback stack, like "en-GB", "fr-FR"
/// The build .
public static FallbackCultureInfo Build(String name, params String[] fallbacks)
{
lock (FallbackCultureInf

Solution

Bugs

So, some interesting bugs. If I specify two cultures that have the same invariant, but are different versions it creates really unpleasant circumstances. (Infinite loops, anyone?)

var cultureInfo2 = FallbackCultureInfo.Build("en-GB", "en-US", "fr-CH");


That creates an infinite loop when rooting through the parent.
So does:

var cultureInfo2 = FallbackCultureInfo.Build("en-GB", "fr-CH", "fr-FR", "de-CH");


Why does this matter? I could see a very real use case being:

cultureInfo = FallbackCultureInfo.Build("en-AU", "en-GB", "en-US");


(Use the Australian English culture, if you can't find it there use Great Britain English culture, if you can't find it there use the United States English culture.) Though, using the neutral (en) as the second in line may very well solve that problem with most strings, but it's still a possibility that this chain could be used and is now broken.

Of course, it's not consistent because of your static member there.

var cultureInfo = new System.Globalization.CultureInfo("en-GB");
cultureInfo = FallbackCultureInfo.Build("it-CH", "fr-CH", "de-CH");
cultureInfo = FallbackCultureInfo.Build("en-GB", "fr-FR", "de-CH");


When looking through all the parents of that second culture set, I don't get the correct tree.

en-GB
en
fr-CH
fr
de-CH
de


But I specified fr-FR for the second fallback!?!?!?!

Of course, we can get even more interesting results with a few other options:

cultureInfo = FallbackCultureInfo.Build("it-CH", "fr-CH", "de-CH");
cultureInfo = FallbackCultureInfo.Build("fr-CH", "it-CH", "de-CH");


fr-CH
fr
de-CH
de


Wait, what? Where did it-CH go?

cultureInfo = FallbackCultureInfo.Build("it-CH", "fr-CH", "de-CH");
cultureInfo = FallbackCultureInfo.Build("fr-CH");


fr-CH
fr
de-CH
de


Ah, I guess I really did need de-CH after all.

While both of these bugs are pretty major, for your situation they're really not something you would look for. You are specifically switching between languages that have different parents, and you're only creating one FallbackCultureInfo. (Which is probably the most likely scenario.)

Review

In C# we prefer the string alias instead of the String type.

Other than that, I have no real issues with the structure of your code, but I do have an issue with how you solved the problem.

Alternate Implementation

From what understand of the documentation, you should be able to get away with making this a lot simpler:


The cultures have a hierarchy in which the parent of a specific culture is a neutral culture, the parent of a neutral culture is the InvariantCulture, and the parent of the InvariantCulture is the invariant culture itself. The parent culture encompasses only the set of information that is common among its children.


If the resources for the specific culture are not available in the system, the resources for the neutral culture are used. If the resources for the neutral culture are not available, the resources embedded in the main assembly are used. For more information on the resource fallback process, see Packaging and Deploying Resources in Desktop Apps.

Basically, you should be able to just work with the Parent property and build from there.

public class NewFallbackCultureInfo : CultureInfo
{
    public NewFallbackCultureInfo FallbackCulture { get; }

    public NewFallbackCultureInfo(string name, params string[] names)
        : base(name)
    {
        if (names.Length > 0)
        {
            FallbackCulture = new NewFallbackCultureInfo(this, names);
        }
    }

    private NewFallbackCultureInfo(CultureInfo sourceCulture, params string[] names)
        : base(sourceCulture.Parent.Name)
    {
        var newNames = new string[names.Length - 1];

        for (int i = 1; i  FallbackCulture ?? base.Parent;
}


Note that we also built this without the .Build pattern, and relied instead on constructors. It's more natural this way, and preserves the original CultureInfo usage.

The only downside I see is that the original parent chain may not be preserved, if it goes more than one level.

We can fix that by modifying our private constructor:

private NewFallbackCultureInfo(CultureInfo sourceCulture, params string[] names)
    : base(sourceCulture.Parent.Name)
{
    if (string.IsNullOrEmpty(base.Parent.Name))
    {
        var newNames = new string[names.Length - 1];

        for (int i = 1; i < names.Length; i++)
        {
            newNames[i - 1] = names[i];
        }

        FallbackCulture = new NewFallbackCultureInfo(names[0], newNames);
    }
    else
    {
        FallbackCulture = new NewFallbackCultureInfo(this, names);
    }
}


When tracing the Parent chain, I found that the chain produced by my version is identical to the chain produced by your version, except it doesn't break when tested against the criteria that broke your version.

I apologize if this felt brutal, but I was actually having

Code Snippets

var cultureInfo2 = FallbackCultureInfo.Build("en-GB", "en-US", "fr-CH");
var cultureInfo2 = FallbackCultureInfo.Build("en-GB", "fr-CH", "fr-FR", "de-CH");
cultureInfo = FallbackCultureInfo.Build("en-AU", "en-GB", "en-US");
var cultureInfo = new System.Globalization.CultureInfo("en-GB");
cultureInfo = FallbackCultureInfo.Build("it-CH", "fr-CH", "de-CH");
cultureInfo = FallbackCultureInfo.Build("en-GB", "fr-FR", "de-CH");
en-GB
en
fr-CH
fr
de-CH
de

Context

StackExchange Code Review Q#147192, answer score: 12

Revisions (0)

No revisions yet.