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.
/// <summary>
/// A <see cref="CultureInfo" /> which switches to another language instead of <see cref="CultureInfo.InvariantCulture" />.
/// </summary>
public class FallbackCultureInfo : CultureInfo
{
private static readonly List<FallbackCultureInfo> CultureInfos = new List<FallbackCultureInfo>();
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;
}
/// <summary>
/// Builds a <see cref="CultureInfo"/> with a custom fallback behavior, which switsches to
/// another language before it gets <see cref="CultureInfo.InvariantCulture"/>.
/// </summary>
/// <example>
/// CultureInfo.CurrentUICulture = FallbackCultureInfo.Build("it-CH", "fr-CH", "de-CH");
/// </example>
/// <remarks>
/// Due to a missing <see cref="CultureInfo" />.operator== we have to ensure a unique instance per name on our own.
/// </remarks>
/// <param name="name">Name of our culture, like "en-US"</param>
/// <param name="fallbacks">Fallback stack, like "en-GB", "fr-FR"</param>
/// <returns>The build <see cref="CultureInfo"/>.</returns>
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<String> names)
{
FallbackCultureInfo result = null;
FallbackCultureInfo lastFallback = null;
foreach (var name in names)
{
result = FallbackCultureInfo.CultureInfos.FirstOrDefault(ci => 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?
1 Answer 1
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 < names.Length; i++)
{
newNames[i - 1] = names[i];
}
FallbackCulture = new NewFallbackCultureInfo(names[0], newNames);
}
public override CultureInfo Parent => 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 a bit of fun with it after I realized what was happening.
-
\$\begingroup\$ Thanks for your review! That's what I requested, it's just code, and it doesn't feel brutal :) Okay - string/String - I agree, but thats just a team internal agreement for syntax highlighting (Which I don't like as well) The static/Build()-pattern is necessary to achieve instance uniqueness as described in its xmldoc. At the beginning, I used the same way you did. But Microsoft distributes with a missing operator== in CultureInfo, while the ResourceManager uses it, which leads to a broken resource stack. I'll review the other parts of your review as soon as my current work has been done . \$\endgroup\$Kelon– Kelon2016年12月29日 13:47:44 +00:00Commented Dec 29, 2016 at 13:47
-
\$\begingroup\$ @Kelon Care to elaborate on what you mean by 'instance uniqueness', and 'broken resource stack'? \$\endgroup\$Der Kommissar– Der Kommissar2016年12月29日 16:00:19 +00:00Commented Dec 29, 2016 at 16:00
-
\$\begingroup\$ Due to the missing operator== overload, the ResourceManager does a ReferenceEquals()-Call instead of an Equals()-Call to find a loaded ResourceSet whenever it searches for a fallback resource value. To reproduce this problem, just change the CurrentUICulture a second time after requesting the resource. Mhm, But Okay, you're creating all fallbacks inside of the constructor. Perhaps thats already a good way to fill this gap - thanks again. \$\endgroup\$Kelon– Kelon2016年12月30日 08:51:51 +00:00Commented Dec 30, 2016 at 8:51
-
\$\begingroup\$ This answer has been selected as the winner of Best of Code Review 2016 — Exterminator. \$\endgroup\$200_success– 200_success2017年01月18日 19:15:14 +00:00Commented Jan 18, 2017 at 19:15
it-CH
and have half the UI actually rendered infr-CH
with a bit ofde-CH
sprinkled in. \$\endgroup\$