diff --git a/Mvc.sln b/Mvc.sln index 485f478ec9..12b6171467 100644 --- a/Mvc.sln +++ b/Mvc.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22808.1 +VisualStudioVersion = 14.0.22823.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}" EndProject @@ -168,6 +168,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.ApiExp EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Abstractions.Test", "test\Microsoft.AspNet.Mvc.Abstractions.Test\Microsoft.AspNet.Mvc.Abstractions.Test.xproj", "{DA000953-7532-4DF5-8DB9-8143DF98D999}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LocalizationWebSite", "test\WebSites\LocalizationWebSite\LocalizationWebSite.xproj", "{FCFE6024-2720-49B4-8257-9DBC6114F0F1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1010,6 +1012,18 @@ Global {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|Mixed Platforms.Build.0 = Release|Any CPU {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|x86.ActiveCfg = Release|Any CPU {DA000953-7532-4DF5-8DB9-8143DF98D999}.Release|x86.Build.0 = Release|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Debug|x86.ActiveCfg = Debug|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Debug|x86.Build.0 = Debug|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Release|Any CPU.Build.0 = Release|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Release|x86.ActiveCfg = Release|Any CPU + {FCFE6024-2720-49B4-8257-9DBC6114F0F1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1092,5 +1106,6 @@ Global {A2B72833-5D70-4C42-AE85-E0319926FB8A} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E} {4C2AD8AB-8AC0-46C4-80C6-C5577C7255F6} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} {DA000953-7532-4DF5-8DB9-8143DF98D999} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1} + {FCFE6024-2720-49B4-8257-9DBC6114F0F1} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C} EndGlobalSection EndGlobal diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index dc2a6feb9f..7106abd18c 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -46,7 +46,7 @@ namespace MvcSample.Web options.Filters.Add(new FormatFilterAttribute()); }); - services.AddMvcLocalization(); + services.AddMvcLocalization(LanguageViewLocationExpanderOption.SubFolder); #if DNX451 // Fully-qualify configuration path to avoid issues in functional tests. Just "config.json" would be fine @@ -59,7 +59,9 @@ namespace MvcSample.Web var configBuilder = new ConfigurationBuilder() .AddJsonFile(configurationPath) .AddEnvironmentVariables(); + var configuration = configBuilder.Build(); + string diSystem; if (configuration.TryGet("DependencyInjection", out diSystem) && diSystem.Equals("AutoFac", StringComparison.OrdinalIgnoreCase)) diff --git a/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpander.cs b/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpander.cs index 4b5c4c5aef..3fcc576e6d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpander.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpander.cs @@ -25,6 +25,24 @@ namespace Microsoft.AspNet.Mvc.Razor public class LanguageViewLocationExpander : IViewLocationExpander { private const string ValueKey = "language"; + private LanguageViewLocationExpanderOption _option; + + /// + /// Instantiates a new instance. + /// + public LanguageViewLocationExpander() + : this(LanguageViewLocationExpanderOption.Suffix) + { + } + + /// + /// Instantiates a new instance. + /// + /// The . + public LanguageViewLocationExpander(LanguageViewLocationExpanderOption option) + { + _option = option; + } /// public void PopulateValues([NotNull] ViewLocationExpanderContext context) @@ -71,7 +89,14 @@ namespace Microsoft.AspNet.Mvc.Razor while (temporaryCultureInfo != temporaryCultureInfo.Parent) { - yield return location.Replace("{0}", temporaryCultureInfo.Name + "/{0}"); + if (_option == LanguageViewLocationExpanderOption.SubFolder) + { + yield return location.Replace("{0}", temporaryCultureInfo.Name + "/{0}"); + } + else + { + yield return location.Replace("{0}", "{0}." + temporaryCultureInfo.Name); + } temporaryCultureInfo = temporaryCultureInfo.Parent; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpanderOption.cs b/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpanderOption.cs new file mode 100644 index 0000000000..71f663e959 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/LanguageViewLocationExpanderOption.cs @@ -0,0 +1,27 @@ +// 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. + +namespace Microsoft.AspNet.Mvc.Razor +{ + /// + /// Specifies the localized view format for . + /// + public enum LanguageViewLocationExpanderOption + { + /// + /// Locale is a subfolder under which the view exisits. + /// + /// + /// Home/Views/en-US/Index.chtml + /// + SubFolder, + + /// + /// Locale is part of the view name as a suffix. + /// + /// + /// Home/Views/Index.en-US.chtml + /// + Suffix + } +} diff --git a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs index 1d1a60b254..cf55c99f7b 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServiceCollectionExtensions.cs @@ -263,11 +263,29 @@ namespace Microsoft.Framework.DependencyInjection services.TryAdd(ServiceDescriptor.Singleton()); } + /// + /// Adds Mvc localization to the application. + /// + /// The . + /// The . public static IServiceCollection AddMvcLocalization([NotNull] this IServiceCollection services) + { + return AddMvcLocalization(services, LanguageViewLocationExpanderOption.Suffix); + } + + /// + /// Adds Mvc localization to the application. + /// + /// The . + /// The view format for localized views. + /// The . + public static IServiceCollection AddMvcLocalization( + [NotNull] this IServiceCollection services, + LanguageViewLocationExpanderOption option) { services.ConfigureRazorViewEngine(options => { - options.ViewLocationExpanders.Add(new LanguageViewLocationExpander()); + options.ViewLocationExpanders.Add(new LanguageViewLocationExpander(option)); }); return services; diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/LocalizationTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/LocalizationTest.cs new file mode 100644 index 0000000000..19eb80bd5d --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/LocalizationTest.cs @@ -0,0 +1,75 @@ +// 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.Reflection; +using System.Threading.Tasks; +using LocalizationWebSite; +using Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNet.Mvc.FunctionalTests +{ + public class LocalizationTest + { + private const string SiteName = nameof(LocalizationWebSite); + private static readonly Assembly _assembly = typeof(LocalizationTest).GetTypeInfo().Assembly; + + private readonly Action _app = new Startup().Configure; + private readonly Action _configureServices = new Startup().ConfigureServices; + + public static IEnumerable LocalizationData + { + get + { + var expected1 = + @" +en-gb-index +partial +mypartial +"; + + yield return new[] { "en-GB", expected1 }; + + var expected2 = + @" +fr-index +fr-partial +mypartial +"; + yield return new[] { "fr", expected2 }; + + var expected3 = + @" +index +partial +mypartial +"; + yield return new[] { "na", expected3 }; + + } + } + + [Theory] + [MemberData(nameof(LocalizationData))] + public async Task Localization_SuffixViewName(string value, string expected) + { + // 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/"); + + // Assert + Assert.Equal(expected, body.Trim()); + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json index 80c6a59a2a..f618d2ffd3 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/project.json @@ -31,6 +31,7 @@ "HtmlGenerationWebSite": "1.0.0", "InlineConstraintsWebSite": "1.0.0", "JsonPatchWebSite": "1.0.0", + "LocalizationWebSite": "1.0.0", "LoggingWebSite": "1.0.0", "LowercaseUrlsWebSite": "1.0.0-*", "Microsoft.AspNet.Mvc": "6.0.0-*", diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs index 2b7dbd6a28..42099969e6 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/LanguageViewLocationExpanderTest.cs @@ -14,6 +14,26 @@ namespace Microsoft.AspNet.Mvc.Razor { yield return new object[] { + LanguageViewLocationExpanderOption.Suffix, + new[] + { + "/Views/{1}/{0}.cshtml", + "/Views/Shared/{0}.cshtml" + }, + new[] + { + "/Views/{1}/{0}.en-GB.cshtml", + "/Views/{1}/{0}.en.cshtml", + "/Views/{1}/{0}.cshtml", + "/Views/Shared/{0}.en-GB.cshtml", + "/Views/Shared/{0}.en.cshtml", + "/Views/Shared/{0}.cshtml" + } + }; + + yield return new object[] + { + LanguageViewLocationExpanderOption.SubFolder, new[] { "/Views/{1}/{0}.cshtml", @@ -32,6 +52,30 @@ namespace Microsoft.AspNet.Mvc.Razor yield return new object[] { + LanguageViewLocationExpanderOption.Suffix, + new[] + { + "/Areas/{2}/Views/{1}/{0}.cshtml", + "/Areas/{2}/Views/Shared/{0}.cshtml", + "/Views/Shared/{0}.cshtml" + }, + new[] + { + "/Areas/{2}/Views/{1}/{0}.en-GB.cshtml", + "/Areas/{2}/Views/{1}/{0}.en.cshtml", + "/Areas/{2}/Views/{1}/{0}.cshtml", + "/Areas/{2}/Views/Shared/{0}.en-GB.cshtml", + "/Areas/{2}/Views/Shared/{0}.en.cshtml", + "/Areas/{2}/Views/Shared/{0}.cshtml", + "/Views/Shared/{0}.en-GB.cshtml", + "/Views/Shared/{0}.en.cshtml", + "/Views/Shared/{0}.cshtml" + } + }; + + yield return new object[] + { + LanguageViewLocationExpanderOption.SubFolder, new[] { "/Areas/{2}/Views/{1}/{0}.cshtml", @@ -82,12 +126,13 @@ namespace Microsoft.AspNet.Mvc.Razor [Theory] [MemberData(nameof(ViewLocationExpanderTestDataWithExpectedValues))] public void ExpandViewLocations_SpecificLocale( + LanguageViewLocationExpanderOption option, IEnumerable viewLocations, IEnumerable expectedViewLocations) { // Arrange var viewLocationExpanderContext = new ViewLocationExpanderContext(new ActionContext(),"testView", false); - var languageViewLocationExpander = new LanguageViewLocationExpander(); + var languageViewLocationExpander = new LanguageViewLocationExpander(option); viewLocationExpanderContext.Values = new Dictionary(); viewLocationExpanderContext.Values["language"] = "en-GB"; diff --git a/test/WebSites/LocalizationWebSite/Controllers/HomeController.cs b/test/WebSites/LocalizationWebSite/Controllers/HomeController.cs new file mode 100644 index 0000000000..400f382eb8 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Controllers/HomeController.cs @@ -0,0 +1,16 @@ +// 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 Microsoft.AspNet.Mvc; + +namespace LocalizationWebSite.Controllers +{ + public class HomeController : Controller + { + // GET: // + public IActionResult Index() + { + return View(); + } + } +} diff --git a/test/WebSites/LocalizationWebSite/LocalizationWebSite.xproj b/test/WebSites/LocalizationWebSite/LocalizationWebSite.xproj new file mode 100644 index 0000000000..228e08b1f9 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/LocalizationWebSite.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + fcfe6024-2720-49b4-8257-9dbc6114f0f1 + LocalizationWebSite + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/test/WebSites/LocalizationWebSite/Startup.cs b/test/WebSites/LocalizationWebSite/Startup.cs new file mode 100644 index 0000000000..b3abd50242 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Startup.cs @@ -0,0 +1,29 @@ +// 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 Microsoft.AspNet.Builder; +using Microsoft.Framework.DependencyInjection; + +namespace LocalizationWebSite +{ + public class Startup + { + // Set up application services + public void ConfigureServices(IServiceCollection services) + { + // Add MVC services to the services container + services.AddMvc(); + services.AddMvcLocalization(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseCultureReplacer(); + + app.UseRequestLocalization(); + + // Add MVC to the request pipeline + app.UseMvcWithDefaultRoute(); + } + } +} diff --git a/test/WebSites/LocalizationWebSite/Views/Home/Index.cshtml b/test/WebSites/LocalizationWebSite/Views/Home/Index.cshtml new file mode 100644 index 0000000000..6cddb0922d --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Views/Home/Index.cshtml @@ -0,0 +1,6 @@ +@{ + Layout = "_MyLayout"; +} +index +@await Html.PartialAsync("_Partial") +@await Html.PartialAsync("_MyPartial") diff --git a/test/WebSites/LocalizationWebSite/Views/Home/Index.en-gb.cshtml b/test/WebSites/LocalizationWebSite/Views/Home/Index.en-gb.cshtml new file mode 100644 index 0000000000..b622ef3293 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Views/Home/Index.en-gb.cshtml @@ -0,0 +1,6 @@ +@{ + Layout = "_MyLayout"; +} +en-gb-index +@await Html.PartialAsync("_Partial") +@await Html.PartialAsync("_MyPartial") diff --git a/test/WebSites/LocalizationWebSite/Views/Home/Index.fr.cshtml b/test/WebSites/LocalizationWebSite/Views/Home/Index.fr.cshtml new file mode 100644 index 0000000000..bed2dcd6a4 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Views/Home/Index.fr.cshtml @@ -0,0 +1,6 @@ +@{ + Layout = "_MyLayout"; +} +fr-index +@await Html.PartialAsync("_Partial") +@await Html.PartialAsync("_MyPartial") diff --git a/test/WebSites/LocalizationWebSite/Views/Home/_MyPartial.cshtml b/test/WebSites/LocalizationWebSite/Views/Home/_MyPartial.cshtml new file mode 100644 index 0000000000..91efca6711 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Views/Home/_MyPartial.cshtml @@ -0,0 +1 @@ +mypartial \ No newline at end of file diff --git a/test/WebSites/LocalizationWebSite/Views/Home/_Partial.cshtml b/test/WebSites/LocalizationWebSite/Views/Home/_Partial.cshtml new file mode 100644 index 0000000000..f46b8786e8 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Views/Home/_Partial.cshtml @@ -0,0 +1 @@ +partial \ No newline at end of file diff --git a/test/WebSites/LocalizationWebSite/Views/Home/_Partial.fr.cshtml b/test/WebSites/LocalizationWebSite/Views/Home/_Partial.fr.cshtml new file mode 100644 index 0000000000..ceaf0a8514 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Views/Home/_Partial.fr.cshtml @@ -0,0 +1 @@ +fr-partial \ No newline at end of file diff --git a/test/WebSites/LocalizationWebSite/Views/Shared/_MyLayout.cshtml b/test/WebSites/LocalizationWebSite/Views/Shared/_MyLayout.cshtml new file mode 100644 index 0000000000..4ef43dfb2d --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Views/Shared/_MyLayout.cshtml @@ -0,0 +1 @@ +@RenderBody() \ No newline at end of file diff --git a/test/WebSites/LocalizationWebSite/Views/Shared/_MyLayout.fr.cshtml b/test/WebSites/LocalizationWebSite/Views/Shared/_MyLayout.fr.cshtml new file mode 100644 index 0000000000..9341e9bdfc --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Views/Shared/_MyLayout.fr.cshtml @@ -0,0 +1 @@ +@RenderBody() \ No newline at end of file diff --git a/test/WebSites/LocalizationWebSite/Views/Shared/_MyPartial.en-gb.cshtml b/test/WebSites/LocalizationWebSite/Views/Shared/_MyPartial.en-gb.cshtml new file mode 100644 index 0000000000..c19523c9e9 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/Views/Shared/_MyPartial.en-gb.cshtml @@ -0,0 +1 @@ +mypartial-en-gb-shared diff --git a/test/WebSites/LocalizationWebSite/project.json b/test/WebSites/LocalizationWebSite/project.json new file mode 100644 index 0000000000..ce5949d698 --- /dev/null +++ b/test/WebSites/LocalizationWebSite/project.json @@ -0,0 +1,20 @@ +{ + "commands": { + "web": "Microsoft.AspNet.Hosting server=Microsoft.AspNet.Server.WebListener server.urls=http://localhost:5001", + "kestrel": "Microsoft.AspNet.Hosting --server Kestrel --server.urls http://localhost:5000" + }, + "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-*", + "Microsoft.AspNet.Server.WebListener": "1.0.0-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-*" + }, + "frameworks": { + "dnx451": { }, + "dnxcore50": { } + }, + "webroot": "wwwroot" +} diff --git a/test/WebSites/LocalizationWebSite/readme.md b/test/WebSites/LocalizationWebSite/readme.md new file mode 100644 index 0000000000..9067d4d5fa --- /dev/null +++ b/test/WebSites/LocalizationWebSite/readme.md @@ -0,0 +1,4 @@ +LocalizationWebSite +=== + +This web site illustrates use cases for Mvc localization. diff --git a/test/WebSites/RazorWebSite/Startup.cs b/test/WebSites/RazorWebSite/Startup.cs index 1381522a5a..42df4057e2 100644 --- a/test/WebSites/RazorWebSite/Startup.cs +++ b/test/WebSites/RazorWebSite/Startup.cs @@ -32,7 +32,7 @@ namespace RazorWebSite options.HtmlHelperOptions.ValidationMessageElement = "validationMessageElement"; options.HtmlHelperOptions.ValidationSummaryMessageElement = "validationSummaryElement"; }); - services.AddMvcLocalization(); + services.AddMvcLocalization(LanguageViewLocationExpanderOption.SubFolder); } public void Configure(IApplicationBuilder app)