patterncsharpModerate
CultureInfo with fallback routing to another language
Viewed 0 times
withcultureinfolanguagefallbackanotherrouting
Problem
Our company needs a localization/translation behavior which allows incomplete (ResX) resources.
If a String
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
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?)
That creates an infinite loop when rooting through the parent.
So does:
Why does this matter? I could see a very real use case being:
(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 (
Of course, it's not consistent because of your
When looking through all the parents of that second culture set, I don't get the correct tree.
But I specified
Of course, we can get even more interesting results with a few other options:
Wait, what? Where did
Ah, I guess I really did need
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
Review
In C# we prefer the
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
Note that we also built this without the
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
When tracing the
I apologize if this felt brutal, but I was actually having
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
deBut 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
deWait, 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
deAh, 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
deContext
StackExchange Code Review Q#147192, answer score: 12
Revisions (0)
No revisions yet.