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
This commit is contained in:
parent
5074bf0c57
commit
1c5362bccb
|
|
@ -18,6 +18,8 @@ namespace Microsoft.AspNet.Localization
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class RequestLocalizationMiddleware
|
public class RequestLocalizationMiddleware
|
||||||
{
|
{
|
||||||
|
private static readonly int MaxCultureFallbackDepth = 5;
|
||||||
|
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly RequestLocalizationOptions _options;
|
private readonly RequestLocalizationOptions _options;
|
||||||
|
|
||||||
|
|
@ -27,9 +29,7 @@ namespace Microsoft.AspNet.Localization
|
||||||
/// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param>
|
/// <param name="next">The <see cref="RequestDelegate"/> representing the next middleware in the pipeline.</param>
|
||||||
/// <param name="options">The <see cref="RequestLocalizationOptions"/> representing the options for the
|
/// <param name="options">The <see cref="RequestLocalizationOptions"/> representing the options for the
|
||||||
/// <see cref="RequestLocalizationMiddleware"/>.</param>
|
/// <see cref="RequestLocalizationMiddleware"/>.</param>
|
||||||
public RequestLocalizationMiddleware(
|
public RequestLocalizationMiddleware(RequestDelegate next, RequestLocalizationOptions options)
|
||||||
RequestDelegate next,
|
|
||||||
RequestLocalizationOptions options)
|
|
||||||
{
|
{
|
||||||
if (next == null)
|
if (next == null)
|
||||||
{
|
{
|
||||||
|
|
@ -75,22 +75,32 @@ namespace Microsoft.AspNet.Localization
|
||||||
CultureInfo uiCultureInfo = null;
|
CultureInfo uiCultureInfo = null;
|
||||||
if (_options.SupportedCultures != null)
|
if (_options.SupportedCultures != null)
|
||||||
{
|
{
|
||||||
cultureInfo = GetCultureInfo(cultures, _options.SupportedCultures);
|
cultureInfo = GetCultureInfo(
|
||||||
|
cultures,
|
||||||
|
_options.SupportedCultures,
|
||||||
|
_options.FallbackToAncestorCulture,
|
||||||
|
currentDepth: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_options.SupportedUICultures != null)
|
if (_options.SupportedUICultures != null)
|
||||||
{
|
{
|
||||||
uiCultureInfo = GetCultureInfo(uiCultures, _options.SupportedUICultures);
|
uiCultureInfo = GetCultureInfo(
|
||||||
|
uiCultures,
|
||||||
|
_options.SupportedUICultures,
|
||||||
|
_options.FallbackToAncestorUICulture,
|
||||||
|
currentDepth: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cultureInfo == null && uiCultureInfo == null)
|
if (cultureInfo == null && uiCultureInfo == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cultureInfo == null && uiCultureInfo != null)
|
if (cultureInfo == null && uiCultureInfo != null)
|
||||||
{
|
{
|
||||||
cultureInfo = _options.DefaultRequestCulture.Culture;
|
cultureInfo = _options.DefaultRequestCulture.Culture;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cultureInfo != null && uiCultureInfo == null)
|
if (cultureInfo != null && uiCultureInfo == null)
|
||||||
{
|
{
|
||||||
uiCultureInfo = _options.DefaultRequestCulture.UICulture;
|
uiCultureInfo = _options.DefaultRequestCulture.UICulture;
|
||||||
|
|
@ -126,15 +136,19 @@ namespace Microsoft.AspNet.Localization
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private CultureInfo GetCultureInfo(IList<string> cultures, IList<CultureInfo> supportedCultures)
|
private static CultureInfo GetCultureInfo(
|
||||||
|
IList<string> cultureNames,
|
||||||
|
IList<CultureInfo> 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
|
// Allow empty string values as they map to InvariantCulture, whereas null culture values will throw in
|
||||||
// the CultureInfo ctor
|
// the CultureInfo ctor
|
||||||
if (culture != null)
|
if (cultureName != null)
|
||||||
{
|
{
|
||||||
var cultureInfo = CultureInfoCache.GetCultureInfo(culture, supportedCultures);
|
var cultureInfo = CultureInfoCache.GetCultureInfo(cultureName, supportedCultures);
|
||||||
if (cultureInfo != null)
|
if (cultureInfo != null)
|
||||||
{
|
{
|
||||||
return cultureInfo;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,33 @@ namespace Microsoft.AspNet.Localization
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="IRequestCultureProvider"/>s is not in the
|
||||||
|
/// <see cref="SupportedCultures"/> list but an ancestor culture is.
|
||||||
|
/// Defaults to <c>true</c>;
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// If this property is <c>true</c> and the application is configured to support the culture "fr", but not the
|
||||||
|
/// culture "fr-FR", and a configured <see cref="IRequestCultureProvider"/> 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".
|
||||||
|
/// </example>
|
||||||
|
public bool FallbackToAncestorCulture { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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 <see cref="IRequestCultureProvider"/>s is not in the
|
||||||
|
/// <see cref="SupportedUICultures"/> list but an ancestor culture is.
|
||||||
|
/// Defaults to <c>true</c>;
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// If this property is <c>true</c> and the application is configured to support the UI culture "fr", but not
|
||||||
|
/// the UI culture "fr-FR", and a configured <see cref="IRequestCultureProvider"/> 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".
|
||||||
|
/// </example>
|
||||||
|
public bool FallbackToAncestorUICulture { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The cultures supported by the application. The <see cref="RequestLocalizationMiddleware"/> will only set
|
/// The cultures supported by the application. The <see cref="RequestLocalizationMiddleware"/> will only set
|
||||||
/// the current request culture to an entry in this list.
|
/// the current request culture to an entry in this list.
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,46 @@ namespace Microsoft.AspNet.Localization.FunctionalTests
|
||||||
"Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder");
|
"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]
|
[ConditionalFact]
|
||||||
[OSSkipCondition(OperatingSystems.Windows)]
|
[OSSkipCondition(OperatingSystems.Windows)]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.CoreCLR)]
|
[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");
|
"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]
|
[ConditionalFact]
|
||||||
[OSSkipCondition(OperatingSystems.Windows)]
|
[OSSkipCondition(OperatingSystems.Windows)]
|
||||||
[FrameworkSkipCondition(RuntimeFrameworks.Mono)]
|
[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");
|
"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]
|
[ConditionalTheory]
|
||||||
[OSSkipCondition(OperatingSystems.Linux)]
|
[OSSkipCondition(OperatingSystems.Linux)]
|
||||||
[OSSkipCondition(OperatingSystems.MacOSX)]
|
[OSSkipCondition(OperatingSystems.MacOSX)]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue