diff --git a/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs b/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs
index e3d5e29830..fc8643fb71 100644
--- a/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs
+++ b/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs
@@ -18,6 +18,8 @@ namespace Microsoft.AspNet.Localization
///
public class RequestLocalizationMiddleware
{
+ private static readonly int MaxCultureFallbackDepth = 5;
+
private readonly RequestDelegate _next;
private readonly RequestLocalizationOptions _options;
@@ -27,9 +29,7 @@ namespace Microsoft.AspNet.Localization
/// The representing the next middleware in the pipeline.
/// The representing the options for the
/// .
- public RequestLocalizationMiddleware(
- RequestDelegate next,
- RequestLocalizationOptions options)
+ public RequestLocalizationMiddleware(RequestDelegate next, RequestLocalizationOptions options)
{
if (next == null)
{
@@ -75,22 +75,32 @@ namespace Microsoft.AspNet.Localization
CultureInfo uiCultureInfo = null;
if (_options.SupportedCultures != null)
{
- cultureInfo = GetCultureInfo(cultures, _options.SupportedCultures);
+ cultureInfo = GetCultureInfo(
+ cultures,
+ _options.SupportedCultures,
+ _options.FallbackToAncestorCulture,
+ currentDepth: 0);
}
if (_options.SupportedUICultures != null)
{
- uiCultureInfo = GetCultureInfo(uiCultures, _options.SupportedUICultures);
+ uiCultureInfo = GetCultureInfo(
+ uiCultures,
+ _options.SupportedUICultures,
+ _options.FallbackToAncestorUICulture,
+ currentDepth: 0);
}
if (cultureInfo == null && uiCultureInfo == null)
{
continue;
}
+
if (cultureInfo == null && uiCultureInfo != null)
{
cultureInfo = _options.DefaultRequestCulture.Culture;
}
+
if (cultureInfo != null && uiCultureInfo == null)
{
uiCultureInfo = _options.DefaultRequestCulture.UICulture;
@@ -126,15 +136,19 @@ namespace Microsoft.AspNet.Localization
#endif
}
- private CultureInfo GetCultureInfo(IList cultures, IList supportedCultures)
+ private static CultureInfo GetCultureInfo(
+ IList cultureNames,
+ IList supportedCultures,
+ bool fallbackToAncestorCulture,
+ int currentDepth)
{
- foreach (var culture in cultures)
+ foreach (var cultureName in cultureNames)
{
// Allow empty string values as they map to InvariantCulture, whereas null culture values will throw in
// the CultureInfo ctor
- if (culture != null)
+ if (cultureName != null)
{
- var cultureInfo = CultureInfoCache.GetCultureInfo(culture, supportedCultures);
+ var cultureInfo = CultureInfoCache.GetCultureInfo(cultureName, supportedCultures);
if (cultureInfo != null)
{
return cultureInfo;
@@ -142,6 +156,39 @@ namespace Microsoft.AspNet.Localization
}
}
+ if (fallbackToAncestorCulture & currentDepth < MaxCultureFallbackDepth)
+ {
+ // Walk backwards through the culture list and remove any root cultures (those with no parent)
+ for (var i = cultureNames.Count - 1; i >= 0; i--)
+ {
+ var cultureName = cultureNames[i];
+ if (cultureName != null)
+ {
+ var lastIndexOfHyphen = cultureName.LastIndexOf('-');
+ if (lastIndexOfHyphen > 0)
+ {
+ // Trim the trailing section from the culture name, e.g. "fr-FR" becomes "fr"
+ cultureNames[i] = cultureName.Substring(0, lastIndexOfHyphen);
+ }
+ else
+ {
+ // The culture had no sections left to trim so remove it from the list of candidates
+ cultureNames.RemoveAt(i);
+ }
+ }
+ else
+ {
+ // Culture name was null so just remove it
+ cultureNames.RemoveAt(i);
+ }
+ }
+
+ if (cultureNames.Count > 0)
+ {
+ return GetCultureInfo(cultureNames, supportedCultures, fallbackToAncestorCulture, currentDepth + 1);
+ }
+ }
+
return null;
}
}
diff --git a/src/Microsoft.AspNet.Localization/RequestLocalizationOptions.cs b/src/Microsoft.AspNet.Localization/RequestLocalizationOptions.cs
index 8c243ea480..db4310d055 100644
--- a/src/Microsoft.AspNet.Localization/RequestLocalizationOptions.cs
+++ b/src/Microsoft.AspNet.Localization/RequestLocalizationOptions.cs
@@ -50,6 +50,33 @@ namespace Microsoft.AspNet.Localization
}
}
+ ///
+ /// Gets or sets a value indicating whether to set a request culture to an ancestor culture in the case the
+ /// culture determined by the configured s is not in the
+ /// list but an ancestor culture is.
+ /// Defaults to true;
+ ///
+ ///
+ /// If this property is true and the application is configured to support the culture "fr", but not the
+ /// culture "fr-FR", and a configured determines a request's culture is
+ /// "fr-FR", then the request's culture will be set to the culture "fr", as it is an ancestor of "fr-FR".
+ ///
+ public bool FallbackToAncestorCulture { get; set; } = true;
+
+ ///
+ /// Gets or sets a value indicating whether to set a request UI culture to an ancestor culture in the case the
+ /// UI culture determined by the configured s is not in the
+ /// list but an ancestor culture is.
+ /// Defaults to true;
+ ///
+ ///
+ /// If this property is true and the application is configured to support the UI culture "fr", but not
+ /// the UI culture "fr-FR", and a configured determines a request's UI
+ /// culture is "fr-FR", then the request's UI culture will be set to the culture "fr", as it is an ancestor of
+ /// "fr-FR".
+ ///
+ public bool FallbackToAncestorUICulture { get; set; } = true;
+
///
/// The cultures supported by the application. The will only set
/// the current request culture to an entry in this list.
diff --git a/test/Microsoft.AspNet.Localization.FunctionalTests/LocalizationTest.cs b/test/Microsoft.AspNet.Localization.FunctionalTests/LocalizationTest.cs
index 27e75f358d..3c58923a86 100644
--- a/test/Microsoft.AspNet.Localization.FunctionalTests/LocalizationTest.cs
+++ b/test/Microsoft.AspNet.Localization.FunctionalTests/LocalizationTest.cs
@@ -30,6 +30,46 @@ namespace Microsoft.AspNet.Localization.FunctionalTests
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
}
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux)]
+ [OSSkipCondition(OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, "http://localhost:5070/", RuntimeArchitecture.x86)]
+ [InlineData(RuntimeFlavor.CoreClr, "http://localhost:5071/", RuntimeArchitecture.x86)]
+ public Task Localization_ResourcesInFolder_ReturnLocalizedValue_WithCultureFallback_Windows(
+ RuntimeFlavor runtimeFlavor,
+ string applicationBaseUrl,
+ RuntimeArchitecture runtimeArchitechture)
+ {
+ var testRunner = new TestRunner();
+ return testRunner.RunTestAndVerifyResponse(
+ runtimeFlavor,
+ runtimeArchitechture,
+ applicationBaseUrl,
+ "ResourcesInFolder",
+ "fr-FR-test",
+ "Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
+ }
+
+ [ConditionalTheory]
+ [OSSkipCondition(OperatingSystems.Linux)]
+ [OSSkipCondition(OperatingSystems.MacOSX)]
+ [InlineData(RuntimeFlavor.Clr, "http://localhost:5070/", RuntimeArchitecture.x86)]
+ [InlineData(RuntimeFlavor.CoreClr, "http://localhost:5071/", RuntimeArchitecture.x86)]
+ public Task Localization_ResourcesInFolder_ReturnNonLocalizedValue_CultureHierarchyTooDeep_Windows(
+ RuntimeFlavor runtimeFlavor,
+ string applicationBaseUrl,
+ RuntimeArchitecture runtimeArchitechture)
+ {
+ var testRunner = new TestRunner();
+ return testRunner.RunTestAndVerifyResponse(
+ runtimeFlavor,
+ runtimeArchitechture,
+ applicationBaseUrl,
+ "ResourcesInFolder",
+ "fr-FR-test-again-too-deep-to-work",
+ "Hello Hello Hello");
+ }
+
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Windows)]
[FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)]
@@ -45,6 +85,21 @@ namespace Microsoft.AspNet.Localization.FunctionalTests
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
}
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)]
+ public Task Localization_ResourcesInFolder_ReturnLocalizedValue_WithCultureFallback_Mono()
+ {
+ var testRunner = new TestRunner();
+ return testRunner.RunTestAndVerifyResponse(
+ RuntimeFlavor.Mono,
+ RuntimeArchitecture.x86,
+ "http://localhost:5072",
+ "ResourcesInFolder",
+ "fr-FR-test",
+ "Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
+ }
+
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Windows)]
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
@@ -60,6 +115,21 @@ namespace Microsoft.AspNet.Localization.FunctionalTests
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
}
+ [ConditionalFact]
+ [OSSkipCondition(OperatingSystems.Windows)]
+ [FrameworkSkipCondition(RuntimeFrameworks.Mono)]
+ public Task Localization_ResourcesInFolder_ReturnLocalizedValue_WithCultureFallback_CoreCLR_NonWindows()
+ {
+ var testRunner = new TestRunner();
+ return testRunner.RunTestAndVerifyResponse(
+ RuntimeFlavor.CoreClr,
+ RuntimeArchitecture.x64,
+ "http://localhost:5073/",
+ "ResourcesInFolder",
+ "fr-FR-test",
+ "Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
+ }
+
[ConditionalTheory]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]