Skip to content
This repository was archived by the owner on Nov 29, 2018. It is now read-only.

Commit 1c5362b

Browse files
committed
Added support for hierarchical culture fallback:
- Enabled by default and configured by RequestLocalizationOptions.FallbackToAncestorCulture/FallbackToAncestorUICulture - Tries all candidate cultures first before trimming the list to parents and trying again, until a match is found, depth limit is reached, or none is found - Updated functional tests to cover fallback case - #112
1 parent 5074bf0 commit 1c5362b

File tree

3 files changed

+153
-9
lines changed

3 files changed

+153
-9
lines changed

src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ namespace Microsoft.AspNet.Localization
1818
/// </summary>
1919
public class RequestLocalizationMiddleware
2020
{
21+
private static readonly int MaxCultureFallbackDepth = 5;
22+
2123
private readonly RequestDelegate _next;
2224
private readonly RequestLocalizationOptions _options;
2325

@@ -27,9 +29,7 @@ public class RequestLocalizationMiddleware
2729
/// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param>
2830
/// <param name="options">The <see cref="RequestLocalizationOptions"/> representing the options for the
2931
/// <see cref="RequestLocalizationMiddleware"/>.</param>
30-
public RequestLocalizationMiddleware(
31-
RequestDelegate next,
32-
RequestLocalizationOptions options)
32+
public RequestLocalizationMiddleware(RequestDelegate next, RequestLocalizationOptions options)
3333
{
3434
if (next == null)
3535
{
@@ -75,22 +75,32 @@ public async Task Invoke(HttpContext context)
7575
CultureInfo uiCultureInfo = null;
7676
if (_options.SupportedCultures != null)
7777
{
78-
cultureInfo = GetCultureInfo(cultures, _options.SupportedCultures);
78+
cultureInfo = GetCultureInfo(
79+
cultures,
80+
_options.SupportedCultures,
81+
_options.FallbackToAncestorCulture,
82+
currentDepth: 0);
7983
}
8084

8185
if (_options.SupportedUICultures != null)
8286
{
83-
uiCultureInfo = GetCultureInfo(uiCultures, _options.SupportedUICultures);
87+
uiCultureInfo = GetCultureInfo(
88+
uiCultures,
89+
_options.SupportedUICultures,
90+
_options.FallbackToAncestorUICulture,
91+
currentDepth: 0);
8492
}
8593

8694
if (cultureInfo == null && uiCultureInfo == null)
8795
{
8896
continue;
8997
}
98+
9099
if (cultureInfo == null && uiCultureInfo != null)
91100
{
92101
cultureInfo = _options.DefaultRequestCulture.Culture;
93102
}
103+
94104
if (cultureInfo != null && uiCultureInfo == null)
95105
{
96106
uiCultureInfo = _options.DefaultRequestCulture.UICulture;
@@ -126,22 +136,59 @@ private static void SetCurrentThreadCulture(RequestCulture requestCulture)
126136
#endif
127137
}
128138

129-
private CultureInfo GetCultureInfo(IList<string> cultures, IList<CultureInfo> supportedCultures)
139+
private static CultureInfo GetCultureInfo(
140+
IList<string> cultureNames,
141+
IList<CultureInfo> supportedCultures,
142+
bool fallbackToAncestorCulture,
143+
int currentDepth)
130144
{
131-
foreach (var culture in cultures)
145+
foreach (var cultureName in cultureNames)
132146
{
133147
// Allow empty string values as they map to InvariantCulture, whereas null culture values will throw in
134148
// the CultureInfo ctor
135-
if (culture != null)
149+
if (cultureName != null)
136150
{
137-
var cultureInfo = CultureInfoCache.GetCultureInfo(culture, supportedCultures);
151+
var cultureInfo = CultureInfoCache.GetCultureInfo(cultureName, supportedCultures);
138152
if (cultureInfo != null)
139153
{
140154
return cultureInfo;
141155
}
142156
}
143157
}
144158

159+
if (fallbackToAncestorCulture & currentDepth < MaxCultureFallbackDepth)
160+
{
161+
// Walk backwards through the culture list and remove any root cultures (those with no parent)
162+
for (var i = cultureNames.Count - 1; i >= 0; i--)
163+
{
164+
var cultureName = cultureNames[i];
165+
if (cultureName != null)
166+
{
167+
var lastIndexOfHyphen = cultureName.LastIndexOf('-');
168+
if (lastIndexOfHyphen > 0)
169+
{
170+
// Trim the trailing section from the culture name, e.g. "fr-FR" becomes "fr"
171+
cultureNames[i] = cultureName.Substring(0, lastIndexOfHyphen);
172+
}
173+
else
174+
{
175+
// The culture had no sections left to trim so remove it from the list of candidates
176+
cultureNames.RemoveAt(i);
177+
}
178+
}
179+
else
180+
{
181+
// Culture name was null so just remove it
182+
cultureNames.RemoveAt(i);
183+
}
184+
}
185+
186+
if (cultureNames.Count > 0)
187+
{
188+
return GetCultureInfo(cultureNames, supportedCultures, fallbackToAncestorCulture, currentDepth + 1);
189+
}
190+
}
191+
145192
return null;
146193
}
147194
}

src/Microsoft.AspNet.Localization/RequestLocalizationOptions.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,33 @@ public RequestCulture DefaultRequestCulture
5050
}
5151
}
5252

53+
/// <summary>
54+
/// Gets or sets a value indicating whether to set a request culture to an ancestor culture in the case the
55+
/// culture determined by the configured <see cref="IRequestCultureProvider"/>s is not in the
56+
/// <see cref="SupportedCultures"/> list but an ancestor culture is.
57+
/// Defaults to <c>true</c>;
58+
/// </summary>
59+
/// <example>
60+
/// If this property is <c>true</c> and the application is configured to support the culture "fr", but not the
61+
/// culture "fr-FR", and a configured <see cref="IRequestCultureProvider"/> determines a request's culture is
62+
/// "fr-FR", then the request's culture will be set to the culture "fr", as it is an ancestor of "fr-FR".
63+
/// </example>
64+
public bool FallbackToAncestorCulture { get; set; } = true;
65+
66+
/// <summary>
67+
/// Gets or sets a value indicating whether to set a request UI culture to an ancestor culture in the case the
68+
/// UI culture determined by the configured <see cref="IRequestCultureProvider"/>s is not in the
69+
/// <see cref="SupportedUICultures"/> list but an ancestor culture is.
70+
/// Defaults to <c>true</c>;
71+
/// </summary>
72+
/// <example>
73+
/// If this property is <c>true</c> and the application is configured to support the UI culture "fr", but not
74+
/// the UI culture "fr-FR", and a configured <see cref="IRequestCultureProvider"/> determines a request's UI
75+
/// culture is "fr-FR", then the request's UI culture will be set to the culture "fr", as it is an ancestor of
76+
/// "fr-FR".
77+
/// </example>
78+
public bool FallbackToAncestorUICulture { get; set; } = true;
79+
5380
/// <summary>
5481
/// The cultures supported by the application. The <see cref="RequestLocalizationMiddleware"/> will only set
5582
/// the current request culture to an entry in this list.

test/Microsoft.AspNet.Localization.FunctionalTests/LocalizationTest.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,46 @@ public Task Localization_ResourcesInFolder_ReturnLocalizedValue_Windows(
3030
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
3131
}
3232

33+
[ConditionalTheory]
34+
[OSSkipCondition(OperatingSystems.Linux)]
35+
[OSSkipCondition(OperatingSystems.MacOSX)]
36+
[InlineData(RuntimeFlavor.Clr, "http://localhost:5070/", RuntimeArchitecture.x86)]
37+
[InlineData(RuntimeFlavor.CoreClr, "http://localhost:5071/", RuntimeArchitecture.x86)]
38+
public Task Localization_ResourcesInFolder_ReturnLocalizedValue_WithCultureFallback_Windows(
39+
RuntimeFlavor runtimeFlavor,
40+
string applicationBaseUrl,
41+
RuntimeArchitecture runtimeArchitechture)
42+
{
43+
var testRunner = new TestRunner();
44+
return testRunner.RunTestAndVerifyResponse(
45+
runtimeFlavor,
46+
runtimeArchitechture,
47+
applicationBaseUrl,
48+
"ResourcesInFolder",
49+
"fr-FR-test",
50+
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
51+
}
52+
53+
[ConditionalTheory]
54+
[OSSkipCondition(OperatingSystems.Linux)]
55+
[OSSkipCondition(OperatingSystems.MacOSX)]
56+
[InlineData(RuntimeFlavor.Clr, "http://localhost:5070/", RuntimeArchitecture.x86)]
57+
[InlineData(RuntimeFlavor.CoreClr, "http://localhost:5071/", RuntimeArchitecture.x86)]
58+
public Task Localization_ResourcesInFolder_ReturnNonLocalizedValue_CultureHierarchyTooDeep_Windows(
59+
RuntimeFlavor runtimeFlavor,
60+
string applicationBaseUrl,
61+
RuntimeArchitecture runtimeArchitechture)
62+
{
63+
var testRunner = new TestRunner();
64+
return testRunner.RunTestAndVerifyResponse(
65+
runtimeFlavor,
66+
runtimeArchitechture,
67+
applicationBaseUrl,
68+
"ResourcesInFolder",
69+
"fr-FR-test-again-too-deep-to-work",
70+
"Hello Hello Hello");
71+
}
72+
3373
[ConditionalFact]
3474
[OSSkipCondition(OperatingSystems.Windows)]
3575
[FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)]
@@ -45,6 +85,21 @@ public Task Localization_ResourcesInFolder_ReturnLocalizedValue_Mono()
4585
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
4686
}
4787

88+
[ConditionalFact]
89+
[OSSkipCondition(OperatingSystems.Windows)]
90+
[FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)]
91+
public Task Localization_ResourcesInFolder_ReturnLocalizedValue_WithCultureFallback_Mono()
92+
{
93+
var testRunner = new TestRunner();
94+
return testRunner.RunTestAndVerifyResponse(
95+
RuntimeFlavor.Mono,
96+
RuntimeArchitecture.x86,
97+
"http://localhost:5072",
98+
"ResourcesInFolder",
99+
"fr-FR-test",
100+
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
101+
}
102+
48103
[ConditionalFact]
49104
[OSSkipCondition(OperatingSystems.Windows)]
50105
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
@@ -60,6 +115,21 @@ public Task Localization_ResourcesInFolder_ReturnLocalizedValue_CoreCLR_NonWindo
60115
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
61116
}
62117

118+
[ConditionalFact]
119+
[OSSkipCondition(OperatingSystems.Windows)]
120+
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
121+
public Task Localization_ResourcesInFolder_ReturnLocalizedValue_WithCultureFallback_CoreCLR_NonWindows()
122+
{
123+
var testRunner = new TestRunner();
124+
return testRunner.RunTestAndVerifyResponse(
125+
RuntimeFlavor.CoreClr,
126+
RuntimeArchitecture.x64,
127+
"http://localhost:5073/",
128+
"ResourcesInFolder",
129+
"fr-FR-test",
130+
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
131+
}
132+
63133
[ConditionalTheory]
64134
[OSSkipCondition(OperatingSystems.Linux)]
65135
[OSSkipCondition(OperatingSystems.MacOSX)]

0 commit comments

Comments
 (0)