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)]