CultureInfo with fallback routing to another language 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.

/// &lt;summary&gt;
/// A &lt;see cref="CultureInfo" /&gt; which switches to another language instead of  &lt;see cref="CultureInfo.InvariantCulture" /&gt;.
/// &lt;/summary&gt;
public class FallbackCultureInfo : CultureInfo
{
    private static readonly List&lt;FallbackCultureInfo&gt; CultureInfos = new List&lt;FallbackCultureInfo&gt;();
    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) &amp;&amp; (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;
    }

    /// &lt;summary&gt;
    /// Builds a &lt;see cref="CultureInfo"/&gt; with a custom fallback behavior, which switsches to 
    /// another language before it gets &lt;see cref="CultureInfo.InvariantCulture"/&gt;.
    /// &lt;/summary&gt;
    /// &lt;example&gt;
    /// CultureInfo.CurrentUICulture = FallbackCultureInfo.Build("it-CH", "fr-CH", "de-CH");
    /// &lt;/example&gt;
    /// &lt;remarks&gt;
    /// Due to a missing &lt;see cref="CultureInfo" /&gt;.operator== we have to ensure a unique instance per name on our own.
    /// &lt;/remarks&gt;
    /// &lt;param name="name"&gt;Name of our culture, like "en-US"&lt;/param&gt;
    /// &lt;param name="fallbacks"&gt;Fallback stack, like "en-GB", "fr-FR"&lt;/param&gt;
    /// &lt;returns&gt;The build &lt;see cref="CultureInfo"/&gt;.&lt;/returns&gt;
    public static FallbackCultureInfo Build(String name, params String[] fallbacks)
    {
        lock (FallbackCultureInfo.CultureInfos)
        {
            return FallbackCultureInfo.QueuedBuild(new[] { name }.Concat(fallbacks).Reverse());
        }
    }

    private static FallbackCultureInfo QueuedBuild(IEnumerable&lt;String&gt; names)
    {
        FallbackCultureInfo result = null;
        FallbackCultureInfo lastFallback = null;
        foreach (var name in names)
        {
            result = FallbackCultureInfo.CultureInfos.FirstOrDefault(ci =&gt; String.Equals(ci.Name, name));
            if (result != null)
            {
                lastFallback = result;
                continue;
            }

            result = lastFallback = new FallbackCultureInfo(name, lastFallback);
            FallbackCultureInfo.CultureInfos.Add(result);
        }

        return result;
    }
}




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


Works like a charm at a first glance. 

But because it is located at a general .Net position, I would like to ask for other opinions about this way to solve the problem. Has anyone done a similiar approach which failed somehow - or even if you havent - do so see any issues with it?
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 &gt; 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 &lt; names.Length; i++)
        {
            newNames[i - 1] = names[i];
        }

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

    public override CultureInfo Parent =&gt; 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 &lt; 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 a bit of fun with it after I realized what was happening.