diff --git a/samples/MvcSample.Web/LanguageViewLocationExpander.cs b/samples/MvcSample.Web/LanguageViewLocationExpander.cs deleted file mode 100644 index 026034c04d..0000000000 --- a/samples/MvcSample.Web/LanguageViewLocationExpander.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNet.Mvc; -using Microsoft.AspNet.Mvc.Razor; - -namespace MvcSample.Web -{ - /// - /// A that replaces adds the language as an extension prefix to view names. - /// - /// - /// For the default case with no areas, views are generated with the following patterns (assuming controller is - /// "Home", action is "Index" and language is "en") - /// Views/Home/en/Action - /// Views/Home/Action - /// Views/Shared/en/Action - /// Views/Shared/Action - /// - public class LanguageViewLocationExpander : IViewLocationExpander - { - private const string ValueKey = "language"; - private readonly Func _valueFactory; - - /// - /// Initializes a new instance of . - /// - /// A factory that provides tbe language to use for expansion. - public LanguageViewLocationExpander(Func valueFactory) - { - _valueFactory = valueFactory; - } - - /// - public void PopulateValues(ViewLocationExpanderContext context) - { - var value = _valueFactory(context.ActionContext); - if (!string.IsNullOrEmpty(value)) - { - context.Values[ValueKey] = value; - } - } - - /// - public virtual IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, - IEnumerable viewLocations) - { - string value; - if (context.Values.TryGetValue(ValueKey, out value)) - { - return ExpandViewLocationsCore(viewLocations, value); - } - - return viewLocations; - } - - private IEnumerable ExpandViewLocationsCore(IEnumerable viewLocations, - string value) - { - foreach (var location in viewLocations) - { - yield return location.Replace("{0}", value + "/{0}"); - yield return location; - } - } - } -} \ No newline at end of file diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index 13c775c228..2a0c067af1 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -46,6 +46,8 @@ namespace MvcSample.Web options.Filters.Add(new FormatFilterAttribute()); }); + services.AddMvcLocalization(); + #if DNX451 // Fully-qualify configuration path to avoid issues in functional tests. Just "config.json" would be fine // but Configuration uses CallContextServiceLocator.Locator.ServiceProvider to get IApplicationEnvironment. @@ -63,12 +65,6 @@ namespace MvcSample.Web diSystem.Equals("AutoFac", StringComparison.OrdinalIgnoreCase)) { _autoFac = true; - services.ConfigureRazorViewEngine(options => - { - var expander = new LanguageViewLocationExpander( - context => context.HttpContext.Request.Query["language"]); - options.ViewLocationExpanders.Insert(0, expander); - }); // Create the autofac container var builder = new ContainerBuilder(); @@ -102,6 +98,7 @@ namespace MvcSample.Web app.UseMiddleware(); } #endif + app.UseRequestLocalization(); app.UseInMemorySession(); app.UseMvc(routes => diff --git a/samples/MvcSample.Web/project.json b/samples/MvcSample.Web/project.json index 75301e7a2d..4da7f577a7 100644 --- a/samples/MvcSample.Web/project.json +++ b/samples/MvcSample.Web/project.json @@ -9,6 +9,7 @@ "dependencies": { "Kestrel": "1.0.0-*", "Microsoft.AspNet.Diagnostics": "1.0.0-*", + "Microsoft.AspNet.Localization": "1.0.0-*", "Microsoft.AspNet.Mvc": "6.0.0-*", "Microsoft.AspNet.Mvc.Xml": "6.0.0-*", "Microsoft.AspNet.Mvc.WebApiCompatShim": "6.0.0-*", diff --git a/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpander.cs b/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpander.cs new file mode 100644 index 0000000000..4b5c4c5aef --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpander.cs @@ -0,0 +1,83 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.Framework.Internal; + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// A that adds the language as an extension prefix to view names. Language + /// that is getting added as extension prefix comes from . + /// + /// + /// For the default case with no areas, views are generated with the following patterns (assuming controller is + /// "Home", action is "Index" and language is "en") + /// Views/Home/en/Action + /// Views/Home/Action + /// Views/Shared/en/Action + /// Views/Shared/Action + /// + public class LanguageViewLocationExpander : IViewLocationExpander + { + private const string ValueKey = "language"; + + /// + public void PopulateValues([NotNull] ViewLocationExpanderContext context) + { + // Using CurrentUICulture so it loads the locale specific resources for the views. +#if DNX451 + context.Values[ValueKey] = Thread.CurrentThread.CurrentUICulture.Name; +#else + context.Values[ValueKey] = CultureInfo.CurrentUICulture.Name; +#endif + } + + /// + public virtual IEnumerable ExpandViewLocations( + [NotNull] ViewLocationExpanderContext context, + [NotNull] IEnumerable viewLocations) + { + string value; + context.Values.TryGetValue(ValueKey, out value); + + if (!string.IsNullOrEmpty(value)) + { + CultureInfo culture; + try + { + culture = new CultureInfo(value); + } + catch (CultureNotFoundException) + { + return viewLocations; + } + + return ExpandViewLocationsCore(viewLocations, culture); + } + + return viewLocations; + } + + private IEnumerable ExpandViewLocationsCore(IEnumerable viewLocations, CultureInfo cultureInfo) + { + foreach (var location in viewLocations) + { + var temporaryCultureInfo = cultureInfo; + + while (temporaryCultureInfo != temporaryCultureInfo.Parent) + { + yield return location.Replace("{0}", temporaryCultureInfo.Name + "/{0}"); + + temporaryCultureInfo = temporaryCultureInfo.Parent; + } + + yield return location; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs index 851889f04e..147bad8336 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs @@ -254,6 +254,16 @@ namespace Microsoft.Framework.DependencyInjection return WithControllersAsServices(services, controllerTypes.Select(type => type.AsType())); } + public static IServiceCollection AddMvcLocalization([NotNull] this IServiceCollection services) + { + services.ConfigureRazorViewEngine(options => + { + options.ViewLocationExpanders.Add(new LanguageViewLocationExpander()); + }); + + return services; + } + private static void ConfigureDefaultServices(IServiceCollection services) { services.AddOptions(); diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/HtmlHelperOptionsTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/HtmlHelperOptionsTest.cs index 9e16cbc932..529b5bb145 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/HtmlHelperOptionsTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/HtmlHelperOptionsTest.cs @@ -60,7 +60,7 @@ False"; An error occurred.
-
+
True @@ -70,7 +70,7 @@ True An error occurred.
-
+
True"; diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs index f92ef73bbb..6224b3d6a3 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/ViewEngineTests.cs @@ -7,6 +7,7 @@ using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.Framework.DependencyInjection; +using Microsoft.Net.Http.Headers; using RazorWebSite; using Xunit; @@ -138,7 +139,7 @@ component-content"; var expected1 = string.Join(Environment.NewLine, "expander-index", "gb-partial"); - yield return new[] { "gb", expected1 }; + yield return new[] { "en-GB", expected1 }; var expected2 = string.Join(Environment.NewLine, "fr-index", @@ -159,10 +160,13 @@ component-content"; // Arrange var server = TestHelper.CreateServer(_app, SiteName, _configureServices); var client = server.CreateClient(); + var cultureCookie = "c=" + value + "|uic=" + value; + client.DefaultRequestHeaders.Add( + "Cookie", + new CookieHeaderValue("ASPNET_CULTURE", cultureCookie).ToString()); // Act - var body = await client.GetStringAsync("http://localhost/TemplateExpander?language-expander-value=" + - value); + var body = await client.GetStringAsync("http://localhost/TemplateExpander"); // Assert Assert.Equal(expected, body.Trim()); @@ -288,7 +292,7 @@ ViewWithNestedLayout-Content View With Layout "; - yield return new[] { "gb", expected1 }; + yield return new[] { "en-GB", expected1 }; yield return new[] { "na", expected1 }; var expected2 = @@ -307,10 +311,13 @@ View With Layout // Arrange var server = TestHelper.CreateServer(_app, SiteName, _configureServices); var client = server.CreateClient(); + var cultureCookie = "c=" + value + "|uic=" + value; + client.DefaultRequestHeaders.Add( + "Cookie", + new CookieHeaderValue("ASPNET_CULTURE", cultureCookie).ToString()); // Act - var body = await client.GetStringAsync("http://localhost/TemplateExpander/ViewWithLayout?language-expander-value=" + - value); + var body = await client.GetStringAsync("http://localhost/TemplateExpander/ViewWithLayout"); // Assert Assert.Equal(expected, body.Trim()); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs new file mode 100644 index 0000000000..2b7dbd6a28 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs @@ -0,0 +1,140 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class LanguageViewLocationExpanderTest + { + public static IEnumerable ViewLocationExpanderTestDataWithExpectedValues + { + get + { + yield return new object[] + { + new[] + { + "/Views/{1}/{0}.cshtml", + "/Views/Shared/{0}.cshtml" + }, + new[] + { + "/Views/{1}/en-GB/{0}.cshtml", + "/Views/{1}/en/{0}.cshtml", + "/Views/{1}/{0}.cshtml", + "/Views/Shared/en-GB/{0}.cshtml", + "/Views/Shared/en/{0}.cshtml", + "/Views/Shared/{0}.cshtml" + } + }; + + yield return new object[] + { + new[] + { + "/Areas/{2}/Views/{1}/{0}.cshtml", + "/Areas/{2}/Views/Shared/{0}.cshtml", + "/Views/Shared/{0}.cshtml" + }, + new[] + { + "/Areas/{2}/Views/{1}/en-GB/{0}.cshtml", + "/Areas/{2}/Views/{1}/en/{0}.cshtml", + "/Areas/{2}/Views/{1}/{0}.cshtml", + "/Areas/{2}/Views/Shared/en-GB/{0}.cshtml", + "/Areas/{2}/Views/Shared/en/{0}.cshtml", + "/Areas/{2}/Views/Shared/{0}.cshtml", + "/Views/Shared/en-GB/{0}.cshtml", + "/Views/Shared/en/{0}.cshtml", + "/Views/Shared/{0}.cshtml" + } + }; + } + } + + public static IEnumerable ViewLocationExpanderTestData + { + get + { + yield return new object[] + { + new[] + { + "/Views/{1}/{0}.cshtml", + "/Views/Shared/{0}.cshtml" + } + }; + + yield return new object[] + { + new[] + { + "/Areas/{2}/Views/{1}/{0}.cshtml", + "/Areas/{2}/Views/Shared/{0}.cshtml", + "/Views/Shared/{0}.cshtml" + } + }; + } + } + + [Theory] + [MemberData(nameof(ViewLocationExpanderTestDataWithExpectedValues))] + public void ExpandViewLocations_SpecificLocale( + IEnumerable viewLocations, + IEnumerable expectedViewLocations) + { + // Arrange + var viewLocationExpanderContext = new ViewLocationExpanderContext(new ActionContext(),"testView", false); + var languageViewLocationExpander = new LanguageViewLocationExpander(); + viewLocationExpanderContext.Values = new Dictionary(); + viewLocationExpanderContext.Values["language"] = "en-GB"; + + // Act + var expandedViewLocations = languageViewLocationExpander.ExpandViewLocations( + viewLocationExpanderContext, + viewLocations); + + // Assert + Assert.Equal(expectedViewLocations, expandedViewLocations); + } + + [Theory] + [MemberData(nameof(ViewLocationExpanderTestData))] + public void ExpandViewLocations_NullContextValue(IEnumerable viewLocations) + { + // Arrange + var viewLocationExpanderContext = new ViewLocationExpanderContext(new ActionContext(), "testView", false); + var languageViewLocationExpander = new LanguageViewLocationExpander(); + viewLocationExpanderContext.Values = new Dictionary(); + + // Act + var expandedViewLocations = languageViewLocationExpander.ExpandViewLocations( + viewLocationExpanderContext, + viewLocations); + + // Assert + Assert.Equal(viewLocations, expandedViewLocations); + } + + [Theory] + [MemberData(nameof(ViewLocationExpanderTestData))] + public void ExpandViewLocations_IncorrectLocaleContextValue(IEnumerable viewLocations) + { + // Arrange + var viewLocationExpanderContext = new ViewLocationExpanderContext(new ActionContext(), "testView", false); + var languageViewLocationExpander = new LanguageViewLocationExpander(); + viewLocationExpanderContext.Values = new Dictionary(); + viewLocationExpanderContext.Values["language"] = "gb"; + + // Act + var expandedViewLocations = languageViewLocationExpander.ExpandViewLocations( + viewLocationExpanderContext, + viewLocations); + + // Assert + Assert.Equal(viewLocations, expandedViewLocations); + } + } +} diff --git a/test/WebSites/RazorWebSite/Services/LanguageViewLocationExpander.cs b/test/WebSites/RazorWebSite/Services/LanguageViewLocationExpander.cs deleted file mode 100644 index 2d8fd074c5..0000000000 --- a/test/WebSites/RazorWebSite/Services/LanguageViewLocationExpander.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using Microsoft.AspNet.Mvc; -using Microsoft.AspNet.Mvc.Razor; - -namespace RazorWebSite -{ - /// - /// A that replaces adds the language as an extension prefix to view names. - /// - /// - /// For the default case with no areas, views are generated with the following patterns (assuming controller is - /// "Home", action is "Index" and language is "en") - /// Views/Home/en/Action - /// Views/Home/Action - /// Views/Shared/en/Action - /// Views/Shared/Action - /// - public class LanguageViewLocationExpander : IViewLocationExpander - { - private const string ValueKey = "language"; - private readonly Func _valueFactory; - - /// - /// Initializes a new instance of . - /// - /// A factory that provides tbe language to use for expansion. - public LanguageViewLocationExpander(Func valueFactory) - { - _valueFactory = valueFactory; - } - - /// - public void PopulateValues(ViewLocationExpanderContext context) - { - var value = _valueFactory(context.ActionContext); - if (!string.IsNullOrEmpty(value)) - { - context.Values[ValueKey] = value; - } - } - - /// - public virtual IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, - IEnumerable viewLocations) - { - string value; - if (context.Values.TryGetValue(ValueKey, out value)) - { - return ExpandViewLocationsCore(viewLocations, value); - } - - return viewLocations; - } - - private IEnumerable ExpandViewLocationsCore(IEnumerable viewLocations, - string value) - { - foreach (var location in viewLocations) - { - yield return location.Replace("{0}", value + "/{0}"); - yield return location; - } - } - } -} \ No newline at end of file diff --git a/test/WebSites/RazorWebSite/Startup.cs b/test/WebSites/RazorWebSite/Startup.cs index 5400d9aed9..2cd20e28a7 100644 --- a/test/WebSites/RazorWebSite/Startup.cs +++ b/test/WebSites/RazorWebSite/Startup.cs @@ -1,7 +1,9 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System.Globalization; using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Localization; using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.Razor; using Microsoft.Framework.DependencyInjection; @@ -20,9 +22,6 @@ namespace RazorWebSite services.AddTransient(); services.Configure(options => { - var expander = new LanguageViewLocationExpander( - context => context.HttpContext.Request.Query["language-expander-value"]); - options.ViewLocationExpanders.Add(expander); options.ViewLocationExpanders.Add(new CustomPartialDirectoryViewLocationExpander()); }); services.ConfigureMvc(options => @@ -33,12 +32,15 @@ namespace RazorWebSite options.HtmlHelperOptions.ValidationMessageElement = "validationMessageElement"; options.HtmlHelperOptions.ValidationSummaryMessageElement = "validationSummaryElement"; }); + services.AddMvcLocalization(); } public void Configure(IApplicationBuilder app) { app.UseCultureReplacer(); + app.UseRequestLocalization(); + // Add MVC to the request pipeline app.UseMvcWithDefaultRoute(); } diff --git a/test/WebSites/RazorWebSite/Views/TemplateExpander/gb/_Partial.cshtml b/test/WebSites/RazorWebSite/Views/TemplateExpander/en-gb/_Partial.cshtml similarity index 100% rename from test/WebSites/RazorWebSite/Views/TemplateExpander/gb/_Partial.cshtml rename to test/WebSites/RazorWebSite/Views/TemplateExpander/en-gb/_Partial.cshtml diff --git a/test/WebSites/RazorWebSite/project.json b/test/WebSites/RazorWebSite/project.json index f2cc80ea16..dfbcb45596 100644 --- a/test/WebSites/RazorWebSite/project.json +++ b/test/WebSites/RazorWebSite/project.json @@ -5,6 +5,7 @@ }, "dependencies": { "Kestrel": "1.0.0-*", + "Microsoft.AspNet.Localization": "1.0.0-*", "Microsoft.AspNet.Mvc": "6.0.0-*", "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", "Microsoft.AspNet.Server.IIS": "1.0.0-*",