diff --git a/src/Middleware/Localization.Routing/src/Microsoft.AspNetCore.Localization.Routing.csproj b/src/Middleware/Localization.Routing/src/Microsoft.AspNetCore.Localization.Routing.csproj new file mode 100644 index 0000000000..e946bf8a00 --- /dev/null +++ b/src/Middleware/Localization.Routing/src/Microsoft.AspNetCore.Localization.Routing.csproj @@ -0,0 +1,17 @@ + + + + Microsoft ASP.NET Core + Provides a request culture provider which gets culture and ui-culture from request's route data. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;localization + + + + + + + + diff --git a/src/Middleware/Localization.Routing/src/RouteDataRequestCultureProvider.cs b/src/Middleware/Localization.Routing/src/RouteDataRequestCultureProvider.cs new file mode 100644 index 0000000000..c14db98721 --- /dev/null +++ b/src/Middleware/Localization.Routing/src/RouteDataRequestCultureProvider.cs @@ -0,0 +1,74 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Localization.Routing +{ + /// + /// Determines the culture information for a request via values in the route data. + /// + public class RouteDataRequestCultureProvider : RequestCultureProvider + { + /// + /// The key that contains the culture name. + /// Defaults to "culture". + /// + public string RouteDataStringKey { get; set; } = "culture"; + + /// + /// The key that contains the UI culture name. If not specified or no value is found, + /// will be used. + /// Defaults to "ui-culture". + /// + public string UIRouteDataStringKey { get; set; } = "ui-culture"; + + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + string culture = null; + string uiCulture = null; + + if (!string.IsNullOrEmpty(RouteDataStringKey)) + { + culture = httpContext.GetRouteValue(RouteDataStringKey)?.ToString(); + } + + if (!string.IsNullOrEmpty(UIRouteDataStringKey)) + { + uiCulture = httpContext.GetRouteValue(UIRouteDataStringKey)?.ToString(); + } + + if (culture == null && uiCulture == null) + { + // No values specified for either so no match + return NullProviderCultureResult; + } + + if (culture != null && uiCulture == null) + { + // Value for culture but not for UI culture so default to culture value for both + uiCulture = culture; + } + + if (culture == null && uiCulture != null) + { + // Value for UI culture but not for culture so default to UI culture value for both + culture = uiCulture; + } + + var providerResultCulture = new ProviderCultureResult(culture, uiCulture); + + return Task.FromResult(providerResultCulture); + } + } +} \ No newline at end of file diff --git a/src/Middleware/Localization.Routing/src/baseline.netcore.json b/src/Middleware/Localization.Routing/src/baseline.netcore.json new file mode 100644 index 0000000000..12c789d631 --- /dev/null +++ b/src/Middleware/Localization.Routing/src/baseline.netcore.json @@ -0,0 +1,80 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Localization.Routing, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Localization.Routing.RouteDataRequestCultureProvider", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Localization.RequestCultureProvider", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DetermineProviderCultureResult", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Localization.IRequestCultureProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RouteDataStringKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RouteDataStringKey", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UIRouteDataStringKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UIRouteDataStringKey", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Middleware/Localization.Routing/test/Microsoft.AspNetCore.Localization.Routing.Tests.csproj b/src/Middleware/Localization.Routing/test/Microsoft.AspNetCore.Localization.Routing.Tests.csproj new file mode 100644 index 0000000000..aa56ca5132 --- /dev/null +++ b/src/Middleware/Localization.Routing/test/Microsoft.AspNetCore.Localization.Routing.Tests.csproj @@ -0,0 +1,13 @@ + + + + $(StandardTestTfms) + + + + + + + + + diff --git a/src/Middleware/Localization.Routing/test/RouteDataRequestCultureProviderTest.cs b/src/Middleware/Localization.Routing/test/RouteDataRequestCultureProviderTest.cs new file mode 100644 index 0000000000..72e7292859 --- /dev/null +++ b/src/Middleware/Localization.Routing/test/RouteDataRequestCultureProviderTest.cs @@ -0,0 +1,195 @@ +// 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 System.Globalization; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Localization.Routing +{ + public class RouteDataRequestCultureProviderTest + { + [Theory] + [InlineData("{culture}/{ui-culture}/hello", "ar-SA/ar-YE/hello", "ar-SA", "ar-YE")] + [InlineData("{CULTURE}/{UI-CULTURE}/hello", "ar-SA/ar-YE/hello", "ar-SA", "ar-YE")] + [InlineData("{culture}/{ui-culture}/hello", "unsupported/unsupported/hello", "en-US", "en-US")] + [InlineData("{culture}/hello", "ar-SA/hello", "ar-SA", "en-US")] + [InlineData("hello", "hello", "en-US", "en-US")] + [InlineData("{ui-culture}/hello", "ar-YE/hello", "en-US", "ar-YE")] + public async Task GetCultureInfo_FromRouteData( + string routeTemplate, + string requestUrl, + string expectedCulture, + string expectedUICulture) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRouter(routes => + { + routes.MapMiddlewareRoute(routeTemplate, fork => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-YE") + } + }; + options.RequestCultureProviders = new[] + { + new RouteDataRequestCultureProvider() + { + Options = options + } + }; + fork.UseRequestLocalization(options); + + fork.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + return context.Response.WriteAsync( + $"{requestCulture.Culture.Name},{requestCulture.UICulture.Name}"); + }); + }); + }); + }) + .ConfigureServices(services => + { + services.AddRouting(); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync(requestUrl); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var data = await response.Content.ReadAsStringAsync(); + Assert.Equal($"{expectedCulture},{expectedUICulture}", data); + } + } + + [Fact] + public async Task GetDefaultCultureInfo_IfCultureKeysAreMissing() + { + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US") + }; + options.RequestCultureProviders = new[] + { + new RouteDataRequestCultureProvider() + { + Options = options + } + }; + app.UseRequestLocalization(options); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + + return context.Response.WriteAsync( + $"{requestCulture.Culture.Name},{requestCulture.UICulture.Name}"); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page"); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var data = await response.Content.ReadAsStringAsync(); + Assert.Equal("en-US,en-US", data); + } + } + + [Theory] + [InlineData("{c}/{uic}/hello", "ar-SA/ar-YE/hello", "ar-SA", "ar-YE")] + [InlineData("{C}/{UIC}/hello", "ar-SA/ar-YE/hello", "ar-SA", "ar-YE")] + [InlineData("{c}/hello", "ar-SA/hello", "ar-SA", "en-US")] + [InlineData("hello", "hello", "en-US", "en-US")] + [InlineData("{uic}/hello", "ar-YE/hello", "en-US", "ar-YE")] + public async Task GetCultureInfo_FromRouteData_WithCustomKeys( + string routeTemplate, + string requestUrl, + string expectedCulture, + string expectedUICulture) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRouter(routes => + { + routes.MapMiddlewareRoute(routeTemplate, fork => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-YE") + } + }; + options.RequestCultureProviders = new[] + { + new RouteDataRequestCultureProvider() + { + Options = options, + RouteDataStringKey = "c", + UIRouteDataStringKey = "uic" + } + }; + fork.UseRequestLocalization(options); + + fork.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + + return context.Response.WriteAsync( + $"{requestCulture.Culture.Name},{requestCulture.UICulture.Name}"); + }); + }); + }); + }) + .ConfigureServices(services => + { + services.AddRouting(); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync(requestUrl); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + var data = await response.Content.ReadAsStringAsync(); + Assert.Equal($"{expectedCulture},{expectedUICulture}", data); + } + } + } +} diff --git a/src/Middleware/Localization/README.md b/src/Middleware/Localization/README.md new file mode 100644 index 0000000000..951344a993 --- /dev/null +++ b/src/Middleware/Localization/README.md @@ -0,0 +1,24 @@ +Localization +============ + +### Localization Samples + +Here are a few samples that demonstrate different localization features including: localized views, localized strings in data annotations, creating custom localization resources ... etc. + + * [Localization.StarterWeb](https://github.com/aspnet/Entropy/tree/dev/samples/Localization.StarterWeb) - comprehensive localization sample demonstrates almost all of the localization features + * [Localization.EntityFramework](https://github.com/aspnet/Entropy/tree/dev/samples/Localization.EntityFramework) - localization sample that uses an EntityFramework based localization provider for resources + * [Localization.CustomResourceManager](https://github.com/aspnet/Entropy/tree/dev/samples/Localization.CustomResourceManager) - localization sample that uses a custom `ResourceManagerStringLocalizer` + +### Localization Providers + +Community projects adapt _RequestCultureProvider_ for determining the culture information of an `HttpRequest`. + + * [My.AspNetCore.Localization.Json](https://github.com/hishamco/My.AspNetCore.Localization.Json) - determines the culture information for a request from a JSON file. + * [My.AspNetCore.Localization.Session](https://github.com/hishamco/My.AspNetCore.Localization.Session) - determines the culture information for a request via values in the session state. + + ### Localization Resources + +Community projects adapt _IStringLocalizer_ for fetching the localiztion resources. + + * [My.Extensions.Localization.Json](https://github.com/hishamco/My.Extensions.Localization.Json) - fetches the localization resources from JSON file(s). + * [OrchardCore.Localization.PortableObject](https://github.com/OrchardCMS/OrchardCore/tree/dev/src/OrchardCore/OrchardCore.Localization.Core/PortableObject) - fetches the localization resources from PO file(s). \ No newline at end of file diff --git a/src/Middleware/Localization/sample/LocalizationSample.csproj b/src/Middleware/Localization/sample/LocalizationSample.csproj new file mode 100644 index 0000000000..77b1172173 --- /dev/null +++ b/src/Middleware/Localization/sample/LocalizationSample.csproj @@ -0,0 +1,16 @@ + + + + netcoreapp2.1;netcoreapp2.0;net461 + + + + + + + + + + + + diff --git a/src/Middleware/Localization/sample/My/Resources/Startup.es-ES.resx b/src/Middleware/Localization/sample/My/Resources/Startup.es-ES.resx new file mode 100644 index 0000000000..0ab8f0ddfb --- /dev/null +++ b/src/Middleware/Localization/sample/My/Resources/Startup.es-ES.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Hola + + \ No newline at end of file diff --git a/src/Middleware/Localization/sample/My/Resources/Startup.fr-FR.resx b/src/Middleware/Localization/sample/My/Resources/Startup.fr-FR.resx new file mode 100644 index 0000000000..9787b469f6 --- /dev/null +++ b/src/Middleware/Localization/sample/My/Resources/Startup.fr-FR.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour + + \ No newline at end of file diff --git a/src/Middleware/Localization/sample/My/Resources/Startup.ja-JP.resx b/src/Middleware/Localization/sample/My/Resources/Startup.ja-JP.resx new file mode 100644 index 0000000000..95869204b3 --- /dev/null +++ b/src/Middleware/Localization/sample/My/Resources/Startup.ja-JP.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + こんにちは + + \ No newline at end of file diff --git a/src/Middleware/Localization/sample/My/Resources/Startup.zh-CN.resx b/src/Middleware/Localization/sample/My/Resources/Startup.zh-CN.resx new file mode 100644 index 0000000000..1da5262c2d --- /dev/null +++ b/src/Middleware/Localization/sample/My/Resources/Startup.zh-CN.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 您好 + + \ No newline at end of file diff --git a/src/Middleware/Localization/sample/My/Resources/Startup.zh.resx b/src/Middleware/Localization/sample/My/Resources/Startup.zh.resx new file mode 100644 index 0000000000..1da5262c2d --- /dev/null +++ b/src/Middleware/Localization/sample/My/Resources/Startup.zh.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 您好 + + \ No newline at end of file diff --git a/src/Middleware/Localization/sample/Startup.cs b/src/Middleware/Localization/sample/Startup.cs new file mode 100644 index 0000000000..8f1bb0d0e6 --- /dev/null +++ b/src/Middleware/Localization/sample/Startup.cs @@ -0,0 +1,155 @@ +// 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.Globalization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +namespace LocalizationSample +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(options => options.ResourcesPath = "My/Resources"); + } + + public void Configure(IApplicationBuilder app, IStringLocalizer SR) + { + var supportedCultures = new [] { "en-US", "en-AU", "en-GB", "es-ES", "ja-JP", "fr-FR", "zh", "zh-CN" }; + app.UseRequestLocalization(options => + options + .AddSupportedCultures(supportedCultures) + .AddSupportedUICultures(supportedCultures) + .SetDefaultCulture(supportedCultures[0]) + // Optionally create an app-specific provider with just a delegate, e.g. look up user preference from DB. + // Inserting it as position 0 ensures it has priority over any of the default providers. + //.RequestCultureProviders.Insert(0, new CustomRequestCultureProvider(async context => + //{ + + //})); + ); + + app.Use(async (context, next) => + { + if (context.Request.Path.Value.EndsWith("favicon.ico")) + { + // Pesky browsers + context.Response.StatusCode = 404; + return; + } + + context.Response.StatusCode = 200; + context.Response.ContentType = "text/html; charset=utf-8"; + + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + + await context.Response.WriteAsync( +$@" + + + {SR["Request Localization"]} + + + +"); + await context.Response.WriteAsync($"

{SR["Request Localization Sample"]}

"); + await context.Response.WriteAsync($"

{SR["Hello"]}

"); + await context.Response.WriteAsync("
"); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync("
"); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync("
"); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($"{SR["reset"]}"); + await context.Response.WriteAsync("
"); + await context.Response.WriteAsync("
"); + await context.Response.WriteAsync(""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($""); + await context.Response.WriteAsync("
Winning provider:{requestCultureFeature.Provider.GetType().Name}
{SR["Current request culture:"]}{requestCulture.Culture.DisplayName} ({requestCulture.Culture})
{SR["Current request UI culture:"]}{requestCulture.UICulture.DisplayName} ({requestCulture.UICulture})
{SR["Current thread culture:"]}{CultureInfo.CurrentCulture.DisplayName} ({CultureInfo.CurrentCulture})
{SR["Current thread UI culture:"]}{CultureInfo.CurrentUICulture.DisplayName} ({CultureInfo.CurrentUICulture})
{SR["Current date (invariant full):"]}{DateTime.Now.ToString("F", CultureInfo.InvariantCulture)}
{SR["Current date (invariant):"]}{DateTime.Now.ToString(CultureInfo.InvariantCulture)}
{SR["Current date (request full):"]}{DateTime.Now.ToString("F")}
{SR["Current date (request):"]}{DateTime.Now.ToString()}
{SR["Current time (invariant):"]}{DateTime.Now.ToString("T", CultureInfo.InvariantCulture)}
{SR["Current time (request):"]}{DateTime.Now.ToString("T")}
{SR["Big number (invariant):"]}{(Math.Pow(2, 42) + 0.42).ToString("N", CultureInfo.InvariantCulture)}
{SR["Big number (request):"]}{(Math.Pow(2, 42) + 0.42).ToString("N")}
{SR["Big number negative (invariant):"]}{(-Math.Pow(2, 42) + 0.42).ToString("N", CultureInfo.InvariantCulture)}
{SR["Big number negative (request):"]}{(-Math.Pow(2, 42) + 0.42).ToString("N")}
{SR["Money (invariant):"]}{2199.50.ToString("C", CultureInfo.InvariantCulture)}
{SR["Money (request):"]}{2199.50.ToString("C")}
{SR["Money negative (invariant):"]}{(-2199.50).ToString("C", CultureInfo.InvariantCulture)}
{SR["Money negative (request):"]}{(-2199.50).ToString("C")}
"); + await context.Response.WriteAsync( +@" +"); + }); + } + + private static async System.Threading.Tasks.Task WriteCultureSelectOptions(HttpContext context) + { + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + } + + public static void Main(string[] args) + { + var config = new ConfigurationBuilder() + .AddCommandLine(args) + .Build(); + + var host = new WebHostBuilder() + .ConfigureLogging(factory => factory.AddConsole()) + .UseKestrel() + .UseConfiguration(config) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/src/Middleware/Localization/src/AcceptLanguageHeaderRequestCultureProvider.cs b/src/Middleware/Localization/src/AcceptLanguageHeaderRequestCultureProvider.cs new file mode 100644 index 0000000000..ecde6636f7 --- /dev/null +++ b/src/Middleware/Localization/src/AcceptLanguageHeaderRequestCultureProvider.cs @@ -0,0 +1,60 @@ +// 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.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// Determines the culture information for a request via the value of the Accept-Language header. + /// + public class AcceptLanguageHeaderRequestCultureProvider : RequestCultureProvider + { + /// + /// The maximum number of values in the Accept-Language header to attempt to create a + /// from for the current request. + /// Defaults to 3. + /// + public int MaximumAcceptLanguageHeaderValuesToTry { get; set; } = 3; + + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var acceptLanguageHeader = httpContext.Request.GetTypedHeaders().AcceptLanguage; + + if (acceptLanguageHeader == null || acceptLanguageHeader.Count == 0) + { + return NullProviderCultureResult; + } + + var languages = acceptLanguageHeader.AsEnumerable(); + + if (MaximumAcceptLanguageHeaderValuesToTry > 0) + { + // We take only the first configured number of languages from the header and then order those that we + // attempt to parse as a CultureInfo to mitigate potentially spinning CPU on lots of parse attempts. + languages = languages.Take(MaximumAcceptLanguageHeaderValuesToTry); + } + + var orderedLanguages = languages.OrderByDescending(h => h, StringWithQualityHeaderValueComparer.QualityComparer) + .Select(x => x.Value).ToList(); + + if (orderedLanguages.Count > 0) + { + return Task.FromResult(new ProviderCultureResult(orderedLanguages)); + } + + return NullProviderCultureResult; + } + } +} diff --git a/src/Middleware/Localization/src/ApplicationBuilderExtensions.cs b/src/Middleware/Localization/src/ApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..9f2a0dac59 --- /dev/null +++ b/src/Middleware/Localization/src/ApplicationBuilderExtensions.cs @@ -0,0 +1,122 @@ +// 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 Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Extension methods for adding the to an application. + /// + public static class ApplicationBuilderExtensions + { + /// + /// Adds the to automatically set culture information for + /// requests based on information provided by the client. + /// + /// The . + /// The . + public static IApplicationBuilder UseRequestLocalization(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + + /// + /// Adds the to automatically set culture information for + /// requests based on information provided by the client. + /// + /// The . + /// The to configure the middleware with. + /// The . + public static IApplicationBuilder UseRequestLocalization( + this IApplicationBuilder app, + RequestLocalizationOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.UseMiddleware(Options.Create(options)); + } + + /// + /// Adds the to automatically set culture information for + /// requests based on information provided by the client. + /// + /// The . + /// + /// + /// This will going to instantiate a new that doesn't come from the services. + /// + /// The . + public static IApplicationBuilder UseRequestLocalization( + this IApplicationBuilder app, + Action optionsAction) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (optionsAction == null) + { + throw new ArgumentNullException(nameof(optionsAction)); + } + + var options = new RequestLocalizationOptions(); + optionsAction.Invoke(options); + + return app.UseMiddleware(Options.Create(options)); + } + + /// + /// Adds the to automatically set culture information for + /// requests based on information provided by the client. + /// + /// The . + /// The culture names to be added by the application, which is represents both supported cultures and UI cultures. + /// The . + /// + /// Note that the first culture is the default culture name. + /// + public static IApplicationBuilder UseRequestLocalization( + this IApplicationBuilder app, + params string[] cultures) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + if (cultures == null) + { + throw new ArgumentNullException(nameof(cultures)); + } + + if (cultures.Length == 0) + { + throw new ArgumentException(Resources.Exception_CulturesShouldNotBeEmpty); + } + + var options = new RequestLocalizationOptions() + .AddSupportedCultures(cultures) + .AddSupportedUICultures(cultures) + .SetDefaultCulture(cultures[0]); + + return app.UseMiddleware(Options.Create(options)); + } + } +} \ No newline at end of file diff --git a/src/Middleware/Localization/src/CookieRequestCultureProvider.cs b/src/Middleware/Localization/src/CookieRequestCultureProvider.cs new file mode 100644 index 0000000000..59a2891dbb --- /dev/null +++ b/src/Middleware/Localization/src/CookieRequestCultureProvider.cs @@ -0,0 +1,122 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// Determines the culture information for a request via the value of a cookie. + /// + public class CookieRequestCultureProvider : RequestCultureProvider + { + private static readonly char[] _cookieSeparator = new[] { '|' }; + private static readonly string _culturePrefix = "c="; + private static readonly string _uiCulturePrefix = "uic="; + + /// + /// Represent the default cookie name used to track the user's preferred culture information, which is ".AspNetCore.Culture". + /// + public static readonly string DefaultCookieName = ".AspNetCore.Culture"; + + /// + /// The name of the cookie that contains the user's preferred culture information. + /// Defaults to . + /// + public string CookieName { get; set; } = DefaultCookieName; + + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var cookie = httpContext.Request.Cookies[CookieName]; + + if (string.IsNullOrEmpty(cookie)) + { + return NullProviderCultureResult; + } + + var providerResultCulture = ParseCookieValue(cookie); + + return Task.FromResult(providerResultCulture); + } + + /// + /// Creates a string representation of a for placement in a cookie. + /// + /// The . + /// The cookie value. + public static string MakeCookieValue(RequestCulture requestCulture) + { + if (requestCulture == null) + { + throw new ArgumentNullException(nameof(requestCulture)); + } + + var seperator = _cookieSeparator[0].ToString(); + + return string.Join(seperator, + $"{_culturePrefix}{requestCulture.Culture.Name}", + $"{_uiCulturePrefix}{requestCulture.UICulture.Name}"); + } + + /// + /// Parses a from the specified cookie value. + /// Returns null if parsing fails. + /// + /// The cookie value to parse. + /// The or null if parsing fails. + public static ProviderCultureResult ParseCookieValue(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var parts = value.Split(_cookieSeparator, StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length != 2) + { + return null; + } + + var potentialCultureName = parts[0]; + var potentialUICultureName = parts[1]; + + if (!potentialCultureName.StartsWith(_culturePrefix) || !potentialUICultureName.StartsWith(_uiCulturePrefix)) + { + return null; + } + + var cultureName = potentialCultureName.Substring(_culturePrefix.Length); + var uiCultureName = potentialUICultureName.Substring(_uiCulturePrefix.Length); + + if (cultureName == null && uiCultureName == null) + { + // No values specified for either so no match + return null; + } + + if (cultureName != null && uiCultureName == null) + { + // Value for culture but not for UI culture so default to culture value for both + uiCultureName = cultureName; + } + + if (cultureName == null && uiCultureName != null) + { + // Value for UI culture but not for culture so default to UI culture value for both + cultureName = uiCultureName; + } + + return new ProviderCultureResult(cultureName, uiCultureName); + } + } +} diff --git a/src/Middleware/Localization/src/CustomRequestCultureProvider.cs b/src/Middleware/Localization/src/CustomRequestCultureProvider.cs new file mode 100644 index 0000000000..a5c9020ce6 --- /dev/null +++ b/src/Middleware/Localization/src/CustomRequestCultureProvider.cs @@ -0,0 +1,42 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// Determines the culture information for a request via the configured delegate. + /// + public class CustomRequestCultureProvider : RequestCultureProvider + { + private readonly Func> _provider; + + /// + /// Creates a new using the specified delegate. + /// + /// The provider delegate. + public CustomRequestCultureProvider(Func> provider) + { + if (provider == null) + { + throw new ArgumentNullException(nameof(provider)); + } + + _provider = provider; + } + + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + return _provider(httpContext); + } + } +} diff --git a/src/Middleware/Localization/src/IRequestCultureFeature.cs b/src/Middleware/Localization/src/IRequestCultureFeature.cs new file mode 100644 index 0000000000..ec0bccbf19 --- /dev/null +++ b/src/Middleware/Localization/src/IRequestCultureFeature.cs @@ -0,0 +1,23 @@ +// 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.AspNetCore.Localization +{ + /// + /// Represents the feature that provides the current request's culture information. + /// + public interface IRequestCultureFeature + { + /// + /// The of the request. + /// + RequestCulture RequestCulture { get; } + + /// + /// The that determined the request's culture information. + /// If the value is null then no provider was used and the request's culture was set to the value of + /// . + /// + IRequestCultureProvider Provider { get; } + } +} \ No newline at end of file diff --git a/src/Middleware/Localization/src/IRequestCultureProvider.cs b/src/Middleware/Localization/src/IRequestCultureProvider.cs new file mode 100644 index 0000000000..ed93931922 --- /dev/null +++ b/src/Middleware/Localization/src/IRequestCultureProvider.cs @@ -0,0 +1,24 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// Represents a provider for determining the culture information of an . + /// + public interface IRequestCultureProvider + { + /// + /// Implements the provider to determine the culture of the given request. + /// + /// The for the request. + /// + /// The determined . + /// Returns null if the provider couldn't determine a . + /// + Task DetermineProviderCultureResult(HttpContext httpContext); + } +} diff --git a/src/Middleware/Localization/src/Internal/RequestCultureProviderLoggerExtensions.cs b/src/Middleware/Localization/src/Internal/RequestCultureProviderLoggerExtensions.cs new file mode 100644 index 0000000000..ee435cda69 --- /dev/null +++ b/src/Middleware/Localization/src/Internal/RequestCultureProviderLoggerExtensions.cs @@ -0,0 +1,38 @@ +// 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.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Localization.Internal +{ + internal static class RequestCultureProviderLoggerExtensions + { + private static readonly Action, Exception> _unsupportedCulture; + private static readonly Action, Exception> _unsupportedUICulture; + + static RequestCultureProviderLoggerExtensions() + { + _unsupportedCulture = LoggerMessage.Define>( + LogLevel.Warning, + 1, + "{requestCultureProvider} returned the following unsupported cultures '{cultures}'."); + _unsupportedUICulture = LoggerMessage.Define>( + LogLevel.Warning, + 2, + "{requestCultureProvider} returned the following unsupported UI Cultures '{uiCultures}'."); + } + + public static void UnsupportedCultures(this ILogger logger, string requestCultureProvider, IList cultures) + { + _unsupportedCulture(logger, requestCultureProvider, cultures, null); + } + + public static void UnsupportedUICultures(this ILogger logger, string requestCultureProvider, IList uiCultures) + { + _unsupportedUICulture(logger, requestCultureProvider, uiCultures, null); + } + } +} diff --git a/src/Middleware/Localization/src/Microsoft.AspNetCore.Localization.csproj b/src/Middleware/Localization/src/Microsoft.AspNetCore.Localization.csproj new file mode 100644 index 0000000000..16c71c9157 --- /dev/null +++ b/src/Middleware/Localization/src/Microsoft.AspNetCore.Localization.csproj @@ -0,0 +1,19 @@ + + + + Microsoft ASP.NET Core + ASP.NET Core middleware for automatically applying culture information to HTTP requests. Culture information can be specified in the HTTP header, query string, cookie, or custom source. + netstandard2.0 + $(NoWarn);CS1591 + true + aspnetcore;localization + + + + + + + + + + diff --git a/src/Middleware/Localization/src/Properties/Resources.Designer.cs b/src/Middleware/Localization/src/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..581e3cda17 --- /dev/null +++ b/src/Middleware/Localization/src/Properties/Resources.Designer.cs @@ -0,0 +1,37 @@ +// +namespace Microsoft.AspNetCore.Localization +{ + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.AspNetCore.Localization.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// Please provide at least one culture. + /// + internal static string Exception_CulturesShouldNotBeEmpty + { + get { return GetString("Exception_CulturesShouldNotBeEmpty"); } + } + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/src/Middleware/Localization/src/ProviderCultureResult.cs b/src/Middleware/Localization/src/ProviderCultureResult.cs new file mode 100644 index 0000000000..2cb8b50e71 --- /dev/null +++ b/src/Middleware/Localization/src/ProviderCultureResult.cs @@ -0,0 +1,67 @@ +// 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 Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// Details about the cultures obtained from . + /// + public class ProviderCultureResult + { + /// + /// Creates a new object that has its and + /// properties set to the same culture value. + /// + /// The name of the culture to be used for formatting, text, i.e. language. + public ProviderCultureResult(StringSegment culture) + : this(new List { culture }, new List { culture }) + { + } + + /// + /// Creates a new object has its and + /// properties set to the respective culture values provided. + /// + /// The name of the culture to be used for formatting. + /// The name of the ui culture to be used for text, i.e. language. + public ProviderCultureResult(StringSegment culture, StringSegment uiCulture) + : this(new List { culture }, new List { uiCulture }) + { + } + + /// + /// Creates a new object that has its and + /// properties set to the same culture value. + /// + /// The list of cultures to be used for formatting, text, i.e. language. + public ProviderCultureResult(IList cultures) + : this(cultures, cultures) + { + } + + /// + /// Creates a new object has its and + /// properties set to the respective culture values provided. + /// + /// The list of cultures to be used for formatting. + /// The list of ui cultures to be used for text, i.e. language. + public ProviderCultureResult(IList cultures, IList uiCultures) + { + Cultures = cultures; + UICultures = uiCultures; + } + + /// + /// Gets the list of cultures to be used for formatting. + /// + public IList Cultures { get; } + + /// + /// Gets the list of ui cultures to be used for text, i.e. language; + /// + public IList UICultures { get; } + } +} diff --git a/src/Middleware/Localization/src/QueryStringRequestCultureProvider.cs b/src/Middleware/Localization/src/QueryStringRequestCultureProvider.cs new file mode 100644 index 0000000000..1f01c0d8c5 --- /dev/null +++ b/src/Middleware/Localization/src/QueryStringRequestCultureProvider.cs @@ -0,0 +1,79 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// Determines the culture information for a request via values in the query string. + /// + public class QueryStringRequestCultureProvider : RequestCultureProvider + { + /// + /// The key that contains the culture name. + /// Defaults to "culture". + /// + public string QueryStringKey { get; set; } = "culture"; + + /// + /// The key that contains the UI culture name. If not specified or no value is found, + /// will be used. + /// Defaults to "ui-culture". + /// + public string UIQueryStringKey { get; set; } = "ui-culture"; + + /// + public override Task DetermineProviderCultureResult(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var request = httpContext.Request; + if (!request.QueryString.HasValue) + { + return NullProviderCultureResult; + } + + string queryCulture = null; + string queryUICulture = null; + + if (!string.IsNullOrWhiteSpace(QueryStringKey)) + { + queryCulture = request.Query[QueryStringKey]; + } + + if (!string.IsNullOrWhiteSpace(UIQueryStringKey)) + { + queryUICulture = request.Query[UIQueryStringKey]; + } + + if (queryCulture == null && queryUICulture == null) + { + // No values specified for either so no match + return NullProviderCultureResult; + } + + if (queryCulture != null && queryUICulture == null) + { + // Value for culture but not for UI culture so default to culture value for both + queryUICulture = queryCulture; + } + + if (queryCulture == null && queryUICulture != null) + { + // Value for UI culture but not for culture so default to UI culture value for both + queryCulture = queryUICulture; + } + + var providerResultCulture = new ProviderCultureResult(queryCulture, queryUICulture); + + return Task.FromResult(providerResultCulture); + } + } +} diff --git a/src/Middleware/Localization/src/RequestCulture.cs b/src/Middleware/Localization/src/RequestCulture.cs new file mode 100644 index 0000000000..5aad1743ca --- /dev/null +++ b/src/Middleware/Localization/src/RequestCulture.cs @@ -0,0 +1,77 @@ +// 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.Globalization; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// Details about the culture for an . + /// + public class RequestCulture + { + /// + /// Creates a new object has its and + /// properties set to the same value. + /// + /// The for the request. + public RequestCulture(CultureInfo culture) + : this(culture, culture) + { + } + + /// + /// Creates a new object has its and + /// properties set to the same value. + /// + /// The culture for the request. + public RequestCulture(string culture) + : this(culture, culture) + { + } + + /// + /// Creates a new object has its and + /// properties set to the respective values provided. + /// + /// The culture for the request to be used for formatting. + /// The culture for the request to be used for text, i.e. language. + public RequestCulture(string culture, string uiCulture) + : this (new CultureInfo(culture), new CultureInfo(uiCulture)) + { + } + + /// + /// Creates a new object has its and + /// properties set to the respective values provided. + /// + /// The for the request to be used for formatting. + /// The for the request to be used for text, i.e. language. + public RequestCulture(CultureInfo culture, CultureInfo uiCulture) + { + if (culture == null) + { + throw new ArgumentNullException(nameof(culture)); + } + + if (uiCulture == null) + { + throw new ArgumentNullException(nameof(uiCulture)); + } + + Culture = culture; + UICulture = uiCulture; + } + + /// + /// Gets the for the request to be used for formatting. + /// + public CultureInfo Culture { get; } + + /// + /// Gets the for the request to be used for text, i.e. language; + /// + public CultureInfo UICulture { get; } + } +} diff --git a/src/Middleware/Localization/src/RequestCultureFeature.cs b/src/Middleware/Localization/src/RequestCultureFeature.cs new file mode 100644 index 0000000000..e20805d7ec --- /dev/null +++ b/src/Middleware/Localization/src/RequestCultureFeature.cs @@ -0,0 +1,35 @@ +// 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; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// Provides the current request's culture information. + /// + public class RequestCultureFeature : IRequestCultureFeature + { + /// + /// Creates a new with the specified . + /// + /// The . + /// The . + public RequestCultureFeature(RequestCulture requestCulture, IRequestCultureProvider provider) + { + if (requestCulture == null) + { + throw new ArgumentNullException(nameof(requestCulture)); + } + + RequestCulture = requestCulture; + Provider = provider; + } + + /// + public RequestCulture RequestCulture { get; } + + /// + public IRequestCultureProvider Provider { get; } + } +} diff --git a/src/Middleware/Localization/src/RequestCultureProvider.cs b/src/Middleware/Localization/src/RequestCultureProvider.cs new file mode 100644 index 0000000000..9430cbdd0a --- /dev/null +++ b/src/Middleware/Localization/src/RequestCultureProvider.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 System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// An abstract base class provider for determining the culture information of an . + /// + public abstract class RequestCultureProvider : IRequestCultureProvider + { + /// + /// Result that indicates that this instance of could not determine the + /// request culture. + /// + protected static readonly Task NullProviderCultureResult = Task.FromResult(default(ProviderCultureResult)); + + /// + /// The current options for the . + /// + public RequestLocalizationOptions Options { get; set; } + + /// + public abstract Task DetermineProviderCultureResult(HttpContext httpContext); + } +} diff --git a/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs b/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs new file mode 100644 index 0000000000..7c2462a4b7 --- /dev/null +++ b/src/Middleware/Localization/src/RequestLocalizationMiddleware.cs @@ -0,0 +1,215 @@ +// 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.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization.Internal; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.Localization +{ + /// + /// Enables automatic setting of the culture for s based on information + /// sent by the client in headers and logic provided by the application. + /// + public class RequestLocalizationMiddleware + { + private static readonly int MaxCultureFallbackDepth = 5; + + private readonly RequestDelegate _next; + private readonly RequestLocalizationOptions _options; + private ILogger _logger; + + /// + /// Creates a new . + /// + /// The representing the next middleware in the pipeline. + /// The representing the options for the + /// . + public RequestLocalizationMiddleware(RequestDelegate next, IOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _next = next ?? throw new ArgumentNullException(nameof(next)); + _options = options.Value; + } + + /// + /// Invokes the logic of the middleware. + /// + /// The . + /// A that completes when the middleware has completed processing. + public async Task Invoke(HttpContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var requestCulture = _options.DefaultRequestCulture; + + IRequestCultureProvider winningProvider = null; + + if (_options.RequestCultureProviders != null) + { + foreach (var provider in _options.RequestCultureProviders) + { + var providerResultCulture = await provider.DetermineProviderCultureResult(context); + if (providerResultCulture == null) + { + continue; + } + var cultures = providerResultCulture.Cultures; + var uiCultures = providerResultCulture.UICultures; + + CultureInfo cultureInfo = null; + CultureInfo uiCultureInfo = null; + if (_options.SupportedCultures != null) + { + cultureInfo = GetCultureInfo( + cultures, + _options.SupportedCultures, + _options.FallBackToParentCultures); + + if (cultureInfo == null) + { + EnsureLogger(context); + _logger?.UnsupportedCultures(provider.GetType().Name, cultures); + } + } + + if (_options.SupportedUICultures != null) + { + uiCultureInfo = GetCultureInfo( + uiCultures, + _options.SupportedUICultures, + _options.FallBackToParentUICultures); + + if (uiCultureInfo == null) + { + EnsureLogger(context); + _logger?.UnsupportedUICultures(provider.GetType().Name, uiCultures); + } + } + + if (cultureInfo == null && uiCultureInfo == null) + { + continue; + } + + if (cultureInfo == null && uiCultureInfo != null) + { + cultureInfo = _options.DefaultRequestCulture.Culture; + } + + if (cultureInfo != null && uiCultureInfo == null) + { + uiCultureInfo = _options.DefaultRequestCulture.UICulture; + } + + var result = new RequestCulture(cultureInfo, uiCultureInfo); + + if (result != null) + { + requestCulture = result; + winningProvider = provider; + break; + } + } + } + + context.Features.Set(new RequestCultureFeature(requestCulture, winningProvider)); + + SetCurrentThreadCulture(requestCulture); + + await _next(context); + } + + private void EnsureLogger(HttpContext context) + { + _logger = _logger ?? context.RequestServices.GetService>(); + } + + private static void SetCurrentThreadCulture(RequestCulture requestCulture) + { + CultureInfo.CurrentCulture = requestCulture.Culture; + CultureInfo.CurrentUICulture = requestCulture.UICulture; + } + + private static CultureInfo GetCultureInfo( + IList cultureNames, + IList supportedCultures, + bool fallbackToParentCultures) + { + 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 (cultureName != null) + { + var cultureInfo = GetCultureInfo(cultureName, supportedCultures, fallbackToParentCultures, currentDepth: 0); + if (cultureInfo != null) + { + return cultureInfo; + } + } + } + + return null; + } + + private static CultureInfo GetCultureInfo(StringSegment name, IList supportedCultures) + { + // Allow only known culture names as this API is called with input from users (HTTP requests) and + // creating CultureInfo objects is expensive and we don't want it to throw either. + if (name == null || supportedCultures == null) + { + return null; + } + var culture = supportedCultures.FirstOrDefault( + supportedCulture => StringSegment.Equals(supportedCulture.Name, name, StringComparison.OrdinalIgnoreCase)); + + if (culture == null) + { + return null; + } + + return CultureInfo.ReadOnly(culture); + } + + private static CultureInfo GetCultureInfo( + StringSegment cultureName, + IList supportedCultures, + bool fallbackToParentCultures, + int currentDepth) + { + var culture = GetCultureInfo(cultureName, supportedCultures); + + if (culture == null && fallbackToParentCultures && currentDepth < MaxCultureFallbackDepth) + { + var lastIndexOfHyphen = cultureName.LastIndexOf('-'); + + if (lastIndexOfHyphen > 0) + { + // Trim the trailing section from the culture name, e.g. "fr-FR" becomes "fr" + var parentCultureName = cultureName.Subsegment(0, lastIndexOfHyphen); + + culture = GetCultureInfo(parentCultureName, supportedCultures, fallbackToParentCultures, currentDepth + 1); + } + } + + return culture; + } + } +} \ No newline at end of file diff --git a/src/Middleware/Localization/src/RequestLocalizationOptions.cs b/src/Middleware/Localization/src/RequestLocalizationOptions.cs new file mode 100644 index 0000000000..16776364f0 --- /dev/null +++ b/src/Middleware/Localization/src/RequestLocalizationOptions.cs @@ -0,0 +1,160 @@ +// 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 Microsoft.AspNetCore.Localization; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Specifies options for the . + /// + public class RequestLocalizationOptions + { + private RequestCulture _defaultRequestCulture = + new RequestCulture(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture); + + /// + /// Creates a new with default values. + /// + public RequestLocalizationOptions() + { + RequestCultureProviders = new List + { + new QueryStringRequestCultureProvider { Options = this }, + new CookieRequestCultureProvider { Options = this }, + new AcceptLanguageHeaderRequestCultureProvider { Options = this } + }; + } + + /// + /// Gets or sets the default culture to use for requests when a supported culture could not be determined by + /// one of the configured s. + /// Defaults to and . + /// + public RequestCulture DefaultRequestCulture + { + get + { + return _defaultRequestCulture; + } + set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + _defaultRequestCulture = value; + } + } + + /// + /// Gets or sets a value indicating whether to set a request culture to an parent culture in the case the + /// culture determined by the configured s is not in the + /// list but a parent culture is. + /// Defaults to true; + /// + /// + /// Note that the parent culture check is done using only the culture name. + /// + /// + /// 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 a parent of "fr-FR". + /// + public bool FallBackToParentCultures { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to set a request UI culture to a parent culture in the case the + /// UI culture determined by the configured s is not in the + /// list but a parent culture is. + /// Defaults to true; + /// + /// + /// Note that the parent culture check is done using ony the culture name. + /// + /// + /// 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 a parent of + /// "fr-FR". + /// + public bool FallBackToParentUICultures { get; set; } = true; + + /// + /// The cultures supported by the application. The will only set + /// the current request culture to an entry in this list. + /// Defaults to . + /// + public IList SupportedCultures { get; set; } = new List { CultureInfo.CurrentCulture }; + + /// + /// The UI cultures supported by the application. The will only set + /// the current request culture to an entry in this list. + /// Defaults to . + /// + public IList SupportedUICultures { get; set; } = new List { CultureInfo.CurrentUICulture }; + + /// + /// An ordered list of providers used to determine a request's culture information. The first provider that + /// returns a non-null result for a given request will be used. + /// Defaults to the following: + /// + /// + /// + /// + /// + /// + public IList RequestCultureProviders { get; set; } + + /// + /// Adds the set of the supported cultures by the application. + /// + /// The cultures to be added. + /// The . + public RequestLocalizationOptions AddSupportedCultures(params string[] cultures) + { + var supportedCultures = new List(); + + foreach (var culture in cultures) + { + supportedCultures.Add(new CultureInfo(culture)); + } + + SupportedCultures = supportedCultures; + return this; + } + + /// + /// Adds the set of the supported UI cultures by the application. + /// + /// The UI cultures to be added. + /// The . + public RequestLocalizationOptions AddSupportedUICultures(params string[] uiCultures) + { + var supportedUICultures = new List(); + foreach (var culture in uiCultures) + { + supportedUICultures.Add(new CultureInfo(culture)); + } + + SupportedUICultures = supportedUICultures; + return this; + } + + /// + /// Set the default culture which is used by the application when a supported culture could not be determined by + /// one of the configured s. + /// + /// The default culture to be set. + /// The . + public RequestLocalizationOptions SetDefaultCulture(string defaultCulture) + { + DefaultRequestCulture = new RequestCulture(defaultCulture); + return this; + } + } +} diff --git a/src/Middleware/Localization/src/Resources.resx b/src/Middleware/Localization/src/Resources.resx new file mode 100644 index 0000000000..17ef00c8c6 --- /dev/null +++ b/src/Middleware/Localization/src/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Please provide at least one culture. + + \ No newline at end of file diff --git a/src/Middleware/Localization/src/baseline.netcore.json b/src/Middleware/Localization/src/baseline.netcore.json new file mode 100644 index 0000000000..3462bcd492 --- /dev/null +++ b/src/Middleware/Localization/src/baseline.netcore.json @@ -0,0 +1,893 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.Localization, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.Builder.ApplicationBuilderExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseRequestLocalization", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseRequestLocalization", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "options", + "Type": "Microsoft.AspNetCore.Builder.RequestLocalizationOptions" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseRequestLocalization", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "optionsAction", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "UseRequestLocalization", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + }, + { + "Name": "cultures", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.RequestLocalizationOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_DefaultRequestCulture", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Localization.RequestCulture", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_DefaultRequestCulture", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Localization.RequestCulture" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FallBackToParentCultures", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_FallBackToParentCultures", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_FallBackToParentUICultures", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_FallBackToParentUICultures", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SupportedCultures", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SupportedCultures", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_SupportedUICultures", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SupportedUICultures", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_RequestCultureProviders", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_RequestCultureProviders", + "Parameters": [ + { + "Name": "value", + "Type": "System.Collections.Generic.IList" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddSupportedCultures", + "Parameters": [ + { + "Name": "cultures", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.RequestLocalizationOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddSupportedUICultures", + "Parameters": [ + { + "Name": "uiCultures", + "Type": "System.String[]", + "IsParams": true + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.RequestLocalizationOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "SetDefaultCulture", + "Parameters": [ + { + "Name": "defaultCulture", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.RequestLocalizationOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.AcceptLanguageHeaderRequestCultureProvider", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Localization.RequestCultureProvider", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DetermineProviderCultureResult", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Localization.IRequestCultureProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaximumAcceptLanguageHeaderValuesToTry", + "Parameters": [], + "ReturnType": "System.Int32", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaximumAcceptLanguageHeaderValuesToTry", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int32" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.CookieRequestCultureProvider", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Localization.RequestCultureProvider", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DetermineProviderCultureResult", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Localization.IRequestCultureProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_CookieName", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_CookieName", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "MakeCookieValue", + "Parameters": [ + { + "Name": "requestCulture", + "Type": "Microsoft.AspNetCore.Localization.RequestCulture" + } + ], + "ReturnType": "System.String", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "ParseCookieValue", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "Microsoft.AspNetCore.Localization.ProviderCultureResult", + "Static": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "DefaultCookieName", + "Parameters": [], + "ReturnType": "System.String", + "Static": true, + "ReadOnly": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.CustomRequestCultureProvider", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Localization.RequestCultureProvider", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DetermineProviderCultureResult", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Localization.IRequestCultureProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "provider", + "Type": "System.Func>" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.IRequestCultureFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestCulture", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Localization.RequestCulture", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Provider", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Localization.IRequestCultureProvider", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.IRequestCultureProvider", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DetermineProviderCultureResult", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.ProviderCultureResult", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Cultures", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UICultures", + "Parameters": [], + "ReturnType": "System.Collections.Generic.IList", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "culture", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "culture", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + }, + { + "Name": "uiCulture", + "Type": "Microsoft.Extensions.Primitives.StringSegment" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "cultures", + "Type": "System.Collections.Generic.IList" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "cultures", + "Type": "System.Collections.Generic.IList" + }, + { + "Name": "uiCultures", + "Type": "System.Collections.Generic.IList" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.QueryStringRequestCultureProvider", + "Visibility": "Public", + "Kind": "Class", + "BaseType": "Microsoft.AspNetCore.Localization.RequestCultureProvider", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "DetermineProviderCultureResult", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Override": true, + "ImplementedInterface": "Microsoft.AspNetCore.Localization.IRequestCultureProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_QueryStringKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_QueryStringKey", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UIQueryStringKey", + "Parameters": [], + "ReturnType": "System.String", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UIQueryStringKey", + "Parameters": [ + { + "Name": "value", + "Type": "System.String" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.RequestCulture", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_Culture", + "Parameters": [], + "ReturnType": "System.Globalization.CultureInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UICulture", + "Parameters": [], + "ReturnType": "System.Globalization.CultureInfo", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "culture", + "Type": "System.Globalization.CultureInfo" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "culture", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "culture", + "Type": "System.String" + }, + { + "Name": "uiCulture", + "Type": "System.String" + } + ], + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "culture", + "Type": "System.Globalization.CultureInfo" + }, + { + "Name": "uiCulture", + "Type": "System.Globalization.CultureInfo" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.RequestCultureFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Localization.IRequestCultureFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_RequestCulture", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Localization.RequestCulture", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Localization.IRequestCultureFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Provider", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Localization.IRequestCultureProvider", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.Localization.IRequestCultureFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "requestCulture", + "Type": "Microsoft.AspNetCore.Localization.RequestCulture" + }, + { + "Name": "provider", + "Type": "Microsoft.AspNetCore.Localization.IRequestCultureProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.RequestCultureProvider", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.Localization.IRequestCultureProvider" + ], + "Members": [ + { + "Kind": "Method", + "Name": "DetermineProviderCultureResult", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Virtual": true, + "Abstract": true, + "ImplementedInterface": "Microsoft.AspNetCore.Localization.IRequestCultureProvider", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_Options", + "Parameters": [], + "ReturnType": "Microsoft.AspNetCore.Builder.RequestLocalizationOptions", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_Options", + "Parameters": [ + { + "Name": "value", + "Type": "Microsoft.AspNetCore.Builder.RequestLocalizationOptions" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Protected", + "GenericParameter": [] + }, + { + "Kind": "Field", + "Name": "NullProviderCultureResult", + "Parameters": [], + "ReturnType": "System.Threading.Tasks.Task", + "Static": true, + "ReadOnly": true, + "Visibility": "Protected", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Localization.RequestLocalizationMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "context", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Middleware/Localization/test/FunctionalTests/LocalizationSampleTest.cs b/src/Middleware/Localization/test/FunctionalTests/LocalizationSampleTest.cs new file mode 100644 index 0000000000..0b5e64f48a --- /dev/null +++ b/src/Middleware/Localization/test/FunctionalTests/LocalizationSampleTest.cs @@ -0,0 +1,38 @@ +// 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.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using LocalizationSample; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace Microsoft.AspNetCore.Localization.FunctionalTests +{ + public class LocalizationSampleTest + { + [Fact] + public async Task LocalizationSampleSmokeTest() + { + // Arrange + var webHostBuilder = new WebHostBuilder().UseStartup(typeof(Startup)); + var testHost = new TestServer(webHostBuilder); + var locale = "fr-FR"; + var client = testHost.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, "My/Resources"); + var cookieValue = $"c={locale}|uic={locale}"; + request.Headers.Add("Cookie", $"{CookieRequestCultureProvider.DefaultCookieName}={cookieValue}"); + + // Act + var response = await client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Contains("

Bonjour

", await response.Content.ReadAsStringAsync()); + } + } +} diff --git a/src/Middleware/Localization/test/FunctionalTests/LocalizationTest.cs b/src/Middleware/Localization/test/FunctionalTests/LocalizationTest.cs new file mode 100644 index 0000000000..5f09115be2 --- /dev/null +++ b/src/Middleware/Localization/test/FunctionalTests/LocalizationTest.cs @@ -0,0 +1,105 @@ +// 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.Net; +using System.Net.Http; +using System.Threading.Tasks; +using LocalizationWebsite; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace Microsoft.AspNetCore.Localization.FunctionalTests +{ + public class LocalizationTest + { + [Fact] + public Task Localization_CustomCulture() + { + return RunTest( + typeof(StartupCustomCulturePreserved), + "en-US", + "kr10.00"); + } + + [Fact] + public Task Localization_GetAllStrings() + { + return RunTest( + typeof(StartupGetAllStrings), + "fr-FR", + "1 Bonjour from Customer in resources folder"); + } + + [Fact] + public Task Localization_ResourcesInClassLibrary_ReturnLocalizedValue() + { + return RunTest( + typeof(StartupResourcesInClassLibrary), + "fr-FR", + "Bonjour from ResourcesClassLibraryNoAttribute Bonjour from ResourcesClassLibraryNoAttribute Bonjour from ResourcesClassLibraryWithAttribute Bonjour from ResourcesClassLibraryWithAttribute"); + } + + [Fact] + public Task Localization_ResourcesInFolder_ReturnLocalizedValue() + { + return RunTest( + typeof(StartupResourcesInFolder), + "fr-FR", + "Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder Hello"); + } + + [Fact] + public Task Localization_ResourcesInFolder_ReturnLocalizedValue_WithCultureFallback() + { + return RunTest( + typeof(StartupResourcesInFolder), + "fr-FR-test", + "Bonjour from StartupResourcesInFolder Bonjour from Test in resources folder Bonjour from Customer in resources folder Hello"); + } + + [Fact] + public Task Localization_ResourcesInFolder_ReturnNonLocalizedValue_CultureHierarchyTooDeep() + { + return RunTest( + typeof(StartupResourcesInFolder), + "fr-FR-test-again-too-deep-to-work", + "Hello Hello Hello Hello"); + } + + [Fact] + public Task Localization_ResourcesAtRootFolder_ReturnLocalizedValue() + { + return RunTest( + typeof(StartupResourcesAtRootFolder), + "fr-FR", + "Bonjour from StartupResourcesAtRootFolder Bonjour from Test in root folder Bonjour from Customer in Models folder"); + } + + [Fact] + public Task Localization_BuilderAPIs() + { + return RunTest( + typeof(StartupBuilderAPIs), + "ar-YE", + "Hello"); + } + + private async Task RunTest(Type startupType, string culture, string expected) + { + var webHostBuilder = new WebHostBuilder().UseStartup(startupType); + var testHost = new TestServer(webHostBuilder); + + var client = testHost.CreateClient(); + var request = new HttpRequestMessage(); + var cookieValue = $"c={culture}|uic={culture}"; + request.Headers.Add("Cookie", $"{CookieRequestCultureProvider.DefaultCookieName}={cookieValue}"); + + var response = await client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(expected, await response.Content.ReadAsStringAsync()); + } + } +} diff --git a/src/Middleware/Localization/test/FunctionalTests/Microsoft.AspNetCore.Localization.FunctionalTests.csproj b/src/Middleware/Localization/test/FunctionalTests/Microsoft.AspNetCore.Localization.FunctionalTests.csproj new file mode 100644 index 0000000000..d3d14f4474 --- /dev/null +++ b/src/Middleware/Localization/test/FunctionalTests/Microsoft.AspNetCore.Localization.FunctionalTests.csproj @@ -0,0 +1,19 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + + + + + diff --git a/src/Middleware/Localization/test/UnitTests/AcceptLanguageHeaderRequestCultureProviderTest.cs b/src/Middleware/Localization/test/UnitTests/AcceptLanguageHeaderRequestCultureProviderTest.cs new file mode 100644 index 0000000000..49a40f0a24 --- /dev/null +++ b/src/Middleware/Localization/test/UnitTests/AcceptLanguageHeaderRequestCultureProviderTest.cs @@ -0,0 +1,157 @@ +// 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 System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace Microsoft.Extensions.Localization +{ + public class AcceptLanguageHeaderRequestCultureProviderTest + { + [Fact] + public async Task GetFallbackLanguage_ReturnsFirstNonNullCultureFromSupportedCultureList() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA"), + new CultureInfo("en-US") + } + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("ar-SA", requestCulture.Culture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("jp,ar-SA,en-US"); + var count = client.DefaultRequestHeaders.AcceptLanguage.Count; + var response = await client.GetAsync(string.Empty); + Assert.Equal(3, count); + } + } + + [Fact] + public async Task GetFallbackLanguage_ReturnsFromSupportedCulture_AcceptLanguageListContainsSupportedCultures() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("fr-FR"), + SupportedCultures = new List + { + new CultureInfo("ar-SA"), + new CultureInfo("en-US") + } + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("ar-SA", requestCulture.Culture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-GB,ar-SA,en-US"); + var count = client.DefaultRequestHeaders.AcceptLanguage.Count; + var response = await client.GetAsync(string.Empty); + } + } + + [Fact] + public async Task GetFallbackLanguage_ReturnsDefault_AcceptLanguageListDoesnotContainSupportedCultures() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("fr-FR"), + SupportedCultures = new List + { + new CultureInfo("ar-SA"), + new CultureInfo("af-ZA") + } + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("fr-FR", requestCulture.Culture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-GB,ar-MA,en-US"); + var count = client.DefaultRequestHeaders.AcceptLanguage.Count; + var response = await client.GetAsync(string.Empty); + Assert.Equal(3, count); + } + } + + [Fact] + public async Task OmitDefaultRequestCultureShouldNotThrowNullReferenceException_And_ShouldGetTheRightCulture() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-YE") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-YE") + } + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + + Assert.Equal("ar-YE", requestCulture.Culture.Name); + Assert.Equal("ar-YE", requestCulture.UICulture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-GB,ar-YE,en-US"); + var count = client.DefaultRequestHeaders.AcceptLanguage.Count; + var response = await client.GetAsync(string.Empty); + Assert.Equal(3, count); + } + } + } +} \ No newline at end of file diff --git a/src/Middleware/Localization/test/UnitTests/CookieRequestCultureProviderTest.cs b/src/Middleware/Localization/test/UnitTests/CookieRequestCultureProviderTest.cs new file mode 100644 index 0000000000..0442cfccbc --- /dev/null +++ b/src/Middleware/Localization/test/UnitTests/CookieRequestCultureProviderTest.cs @@ -0,0 +1,254 @@ +// 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 System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.Extensions.Localization +{ + public class CookieRequestCultureProviderTest + { + [Fact] + public async Task GetCultureInfoFromPersistentCookie() + { + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-SA") + } + }; + var provider = new CookieRequestCultureProvider + { + CookieName = "Preferences" + }; + options.RequestCultureProviders.Insert(0, provider); + + app.UseRequestLocalization(options); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("ar-SA", requestCulture.Culture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var culture = new CultureInfo("ar-SA"); + var requestCulture = new RequestCulture(culture); + var value = CookieRequestCultureProvider.MakeCookieValue(requestCulture); + client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue("Preferences", value).ToString()); + var response = await client.GetAsync(string.Empty); + Assert.Equal("c=ar-SA|uic=ar-SA", value); + } + } + + [Fact] + public async Task GetDefaultCultureInfoIfCultureKeysAreMissingOrInvalid() + { + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-SA") + } + }; + var provider = new CookieRequestCultureProvider + { + CookieName = "Preferences" + }; + options.RequestCultureProviders.Insert(0, provider); + app.UseRequestLocalization(options); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("en-US", requestCulture.Culture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + + client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue("Preferences", "uic=ar-SA").ToString()); + var response = await client.GetAsync(string.Empty); + } + } + + [Fact] + public async Task GetDefaultCultureInfoIfCookieDoesNotExist() + { + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-SA") + } + }; + var provider = new CookieRequestCultureProvider + { + CookieName = "Preferences" + }; + options.RequestCultureProviders.Insert(0, provider); + app.UseRequestLocalization(options); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("en-US", requestCulture.Culture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync(string.Empty); + } + } + + [Fact] + public async Task RequestLocalizationMiddleware_LogsWarningsForUnsupportedCultures() + { + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-YE") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-YE") + } + }; + var provider = new CookieRequestCultureProvider + { + CookieName = "Preferences" + }; + options.RequestCultureProviders.Insert(0, provider); + app.UseRequestLocalization(options); + app.Run(context => Task.CompletedTask); + }) + .ConfigureServices(services => + { + services.AddSingleton(typeof(ILoggerFactory), loggerFactory); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var culture = "??"; + var uiCulture = "ar-YE"; + client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue("Preferences", $"c={culture}|uic={uiCulture}").ToString()); + + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + } + + var expectedMessage = $"{nameof(CookieRequestCultureProvider)} returned the following unsupported cultures '??'."; + + var write = Assert.Single(sink.Writes); + Assert.Equal(LogLevel.Warning, write.LogLevel); + Assert.Equal(expectedMessage, write.State.ToString()); + } + + [Fact] + public async Task RequestLocalizationMiddleware_LogsWarningsForUnsupportedUICultures() + { + var sink = new TestSink( + TestSink.EnableWithTypeName, + TestSink.EnableWithTypeName); + var loggerFactory = new TestLoggerFactory(sink, enabled: true); + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-YE") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-YE") + } + }; + var provider = new CookieRequestCultureProvider + { + CookieName = "Preferences" + }; + options.RequestCultureProviders.Insert(0, provider); + app.UseRequestLocalization(options); + app.Run(context => Task.CompletedTask); + }) + .ConfigureServices(services => + { + services.AddSingleton(typeof(ILoggerFactory), loggerFactory); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var culture = "ar-YE"; + var uiCulture = "??"; + client.DefaultRequestHeaders.Add("Cookie", new CookieHeaderValue("Preferences", $"c={culture}|uic={uiCulture}").ToString()); + + var response = await client.GetAsync(string.Empty); + response.EnsureSuccessStatusCode(); + } + + var expectedMessage = $"{nameof(CookieRequestCultureProvider)} returned the following unsupported UI Cultures '??'."; + var write = Assert.Single(sink.Writes); + Assert.Equal(LogLevel.Warning, write.LogLevel); + Assert.Equal(expectedMessage, write.State.ToString()); + } + } +} \ No newline at end of file diff --git a/src/Middleware/Localization/test/UnitTests/CustomRequestCultureProviderTest.cs b/src/Middleware/Localization/test/UnitTests/CustomRequestCultureProviderTest.cs new file mode 100644 index 0000000000..2cd5cf7177 --- /dev/null +++ b/src/Middleware/Localization/test/UnitTests/CustomRequestCultureProviderTest.cs @@ -0,0 +1,72 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace Microsoft.Extensions.Localization +{ + public class CustomRequestCultureProviderTest + { + [Fact] + public async Task CustomRequestCultureProviderThatGetsCultureInfoFromUrl() + { + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar") + }, + SupportedUICultures = new List + { + new CultureInfo("ar") + } + }; + options.RequestCultureProviders.Insert(0, new CustomRequestCultureProvider(context => + { + var culture = GetCultureInfoFromUrl(context, options.SupportedCultures); + var requestCulture = new ProviderCultureResult(culture); + return Task.FromResult(requestCulture); + })); + app.UseRequestLocalization(options); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("ar", requestCulture.Culture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/ar/page"); + } + } + + private string GetCultureInfoFromUrl(HttpContext context, IList supportedCultures) + { + var currentCulture = "en"; + var segments = context.Request.Path.Value.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + if (segments.Length > 1 && segments[0].Length == 2) + { + currentCulture = segments[0]; + } + + return currentCulture; + } + } +} \ No newline at end of file diff --git a/src/Middleware/Localization/test/UnitTests/Microsoft.AspNetCore.Localization.Tests.csproj b/src/Middleware/Localization/test/UnitTests/Microsoft.AspNetCore.Localization.Tests.csproj new file mode 100644 index 0000000000..8a0fb85de7 --- /dev/null +++ b/src/Middleware/Localization/test/UnitTests/Microsoft.AspNetCore.Localization.Tests.csproj @@ -0,0 +1,15 @@ + + + + $(StandardTestTfms) + + + + + + + + + + + diff --git a/src/Middleware/Localization/test/UnitTests/QueryStringRequestCultureProviderTest.cs b/src/Middleware/Localization/test/UnitTests/QueryStringRequestCultureProviderTest.cs new file mode 100644 index 0000000000..9ad34e32e2 --- /dev/null +++ b/src/Middleware/Localization/test/UnitTests/QueryStringRequestCultureProviderTest.cs @@ -0,0 +1,298 @@ +// 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 System.Globalization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Localization; +using Microsoft.AspNetCore.TestHost; +using Xunit; + +namespace Microsoft.Extensions.Localization +{ + public class QueryStringRequestCultureProviderTest + { + [Fact] + public async Task GetCultureInfoFromQueryString() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-YE") + } + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("ar-SA", requestCulture.Culture.Name); + Assert.Equal("ar-YE", requestCulture.UICulture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page?culture=ar-SA&ui-culture=ar-YE"); + } + } + + [Fact] + public async Task GetDefaultCultureInfoIfCultureKeysAreMissing() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US") + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("en-US", requestCulture.Culture.Name); + Assert.Equal("en-US", requestCulture.UICulture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page"); + } + } + + [Fact] + public async Task GetDefaultCultureInfoIfCultureIsInSupportedCultureList() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-SA") + } + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("en-US", requestCulture.Culture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page?culture=ar-XY&ui-culture=ar-SA"); + } + } + + [Fact] + public async Task GetDefaultCultureInfoIfUICultureIsNotInSupportedList() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-SA") + } + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("en-US", requestCulture.UICulture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page?culture=ar-SA&ui-culture=ar-XY"); + } + } + + [Fact] + public async Task GetSameCultureInfoIfCultureKeyIsMissing() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-SA") + } + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("ar-SA", requestCulture.Culture.Name); + Assert.Equal("ar-SA", requestCulture.UICulture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page?ui-culture=ar-SA"); + } + } + + [Fact] + public async Task GetSameCultureInfoIfUICultureKeyIsMissing() + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-SA") + } + }); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("ar-SA", requestCulture.Culture.Name); + Assert.Equal("ar-SA", requestCulture.UICulture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page?culture=ar-SA"); + } + } + + [Fact] + public async Task GetCultureInfoFromQueryStringWithCustomKeys() + { + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("ar-SA") + }, + SupportedUICultures = new List + { + new CultureInfo("ar-YE") + } + }; + var provider = new QueryStringRequestCultureProvider(); + provider.QueryStringKey = "c"; + provider.UIQueryStringKey = "uic"; + options.RequestCultureProviders.Insert(0, provider); + app.UseRequestLocalization(options); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("ar-SA", requestCulture.Culture.Name); + Assert.Equal("ar-YE", requestCulture.UICulture.Name); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page?c=ar-SA&uic=ar-YE"); + } + } + + [Fact] + public async Task GetTheRightCultureInfoRegardlessOfCultureNameCasing() + { + var builder = new WebHostBuilder() + .Configure(app => + { + var options = new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List + { + new CultureInfo("FR") + }, + SupportedUICultures = new List + { + new CultureInfo("FR") + } + }; + var provider = new QueryStringRequestCultureProvider(); + + provider.QueryStringKey = "c"; + provider.UIQueryStringKey = "uic"; + options.RequestCultureProviders.Insert(0, provider); + app.UseRequestLocalization(options); + app.Run(context => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + Assert.Equal("fr", requestCulture.Culture.ToString()); + Assert.Equal("fr", requestCulture.UICulture.ToString()); + return Task.FromResult(0); + }); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var response = await client.GetAsync("/page?c=FR&uic=FR"); + } + } + } +} \ No newline at end of file diff --git a/src/Middleware/Localization/test/UnitTests/RequestLocalizationOptionsTest.cs b/src/Middleware/Localization/test/UnitTests/RequestLocalizationOptionsTest.cs new file mode 100644 index 0000000000..c33a673321 --- /dev/null +++ b/src/Middleware/Localization/test/UnitTests/RequestLocalizationOptionsTest.cs @@ -0,0 +1,136 @@ +// 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.Globalization; +using System.Linq; +using Microsoft.AspNetCore.Builder; +using Xunit; + +namespace Microsoft.AspNetCore.Localization +{ + public class RequestLocalizationOptionsTest : IDisposable + { + private readonly CultureInfo _initialCulture; + private readonly CultureInfo _initialUICulture; + + public RequestLocalizationOptionsTest() + { + _initialCulture = CultureInfo.CurrentCulture; + _initialUICulture = CultureInfo.CurrentUICulture; + } + + [Fact] + public void DefaultRequestCulture_DefaultsToCurrentCulture() + { + // Arrange/Act + var options = new RequestLocalizationOptions(); + + // Assert + Assert.NotNull(options.DefaultRequestCulture); + Assert.Equal(CultureInfo.CurrentCulture, options.DefaultRequestCulture.Culture); + Assert.Equal(CultureInfo.CurrentUICulture, options.DefaultRequestCulture.UICulture); + } + + [Fact] + public void DefaultRequestCulture_DefaultsToCurrentCultureWhenExplicitlySet() + { + // Arrange + var explicitCulture = new CultureInfo("fr-FR"); + CultureInfo.CurrentCulture = explicitCulture; + CultureInfo.CurrentUICulture = explicitCulture; + + // Act + var options = new RequestLocalizationOptions(); + + // Assert + Assert.Equal(explicitCulture, options.DefaultRequestCulture.Culture); + Assert.Equal(explicitCulture, options.DefaultRequestCulture.UICulture); + } + + [Fact] + public void DefaultRequestCulture_ThrowsWhenTryingToSetToNull() + { + // Arrange + var options = new RequestLocalizationOptions(); + + // Act/Assert + Assert.Throws(() => options.DefaultRequestCulture = null); + } + + [Fact] + public void SupportedCultures_DefaultsToCurrentCulture() + { + // Arrange/Act + var options = new RequestLocalizationOptions(); + + // Assert + Assert.Collection(options.SupportedCultures, item => Assert.Equal(CultureInfo.CurrentCulture, item)); + Assert.Collection(options.SupportedUICultures, item => Assert.Equal(CultureInfo.CurrentUICulture, item)); + } + + [Fact] + public void SupportedCultures_DefaultsToCurrentCultureWhenExplicitlySet() + { + // Arrange + var explicitCulture = new CultureInfo("fr-FR"); + CultureInfo.CurrentCulture = explicitCulture; + CultureInfo.CurrentUICulture = explicitCulture; + + // Act + var options = new RequestLocalizationOptions(); + + // Assert + Assert.Collection(options.SupportedCultures, item => Assert.Equal(explicitCulture, item)); + Assert.Collection(options.SupportedUICultures, item => Assert.Equal(explicitCulture, item)); + } + + [Fact] + public void BuilderAPIs_AddSupportedCultures() + { + // Arrange + var supportedCultures = new[] { "en-US", "ar-YE" }; + + // Act + var options = new RequestLocalizationOptions() + .AddSupportedCultures(supportedCultures); + + // Assert + Assert.Equal(supportedCultures, options.SupportedCultures.Select(c => c.Name)); + } + + [Fact] + public void BuilderAPIs_AddSupportedUICultures() + { + // Arrange + var supportedUICultures = new[] { "en-US", "ar-YE" }; + + // Act + var options = new RequestLocalizationOptions() + .AddSupportedUICultures(supportedUICultures); + + // Assert + Assert.Equal(supportedUICultures, options.SupportedUICultures.Select(c => c.Name)); + } + + [Fact] + public void BuilderAPIs_SetDefaultCulture() + { + // Arrange + var defaultCulture = "ar-YE"; + + // Act + var options = new RequestLocalizationOptions() + .SetDefaultCulture(defaultCulture); + + // Assert + Assert.Equal(defaultCulture, options.DefaultRequestCulture.Culture.Name); + } + + public void Dispose() + { + CultureInfo.CurrentCulture = _initialCulture; + CultureInfo.CurrentUICulture = _initialUICulture; + } + } +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/LocalizationWebsite.csproj b/src/Middleware/Localization/testassets/LocalizationWebsite/LocalizationWebsite.csproj new file mode 100644 index 0000000000..409af955a7 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/LocalizationWebsite.csproj @@ -0,0 +1,21 @@ + + + + netcoreapp2.0;net461 + + + + + + + + + + + + + + + + + diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Models/Customer.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/Models/Customer.cs new file mode 100644 index 0000000000..b38d34e7f1 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Models/Customer.cs @@ -0,0 +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. + +namespace LocalizationWebsite.Models +{ + public class Customer + { + } +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Models/Customer.fr-FR.resx b/src/Middleware/Localization/testassets/LocalizationWebsite/Models/Customer.fr-FR.resx new file mode 100644 index 0000000000..1236654def --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Models/Customer.fr-FR.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour from Customer in Models folder + + \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Program.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/Program.cs new file mode 100644 index 0000000000..7975594760 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Program.cs @@ -0,0 +1,32 @@ +// 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.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace LocalizationWebsite +{ + public static class Program + { + public static void Main(string[] args) + { + var config = new ConfigurationBuilder() + .AddCommandLine(args) + .Build(); + + var host = new WebHostBuilder() + .ConfigureLogging((_, factory) => + { + factory.AddConsole(); + factory.AddFilter("Console", level => level >= LogLevel.Warning); + }) + .UseKestrel() + .UseConfiguration(config) + .UseStartup("LocalizationWebsite") + .Build(); + + host.Run(); + } + } +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/Models.Customer.fr-FR.resx b/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/Models.Customer.fr-FR.resx new file mode 100644 index 0000000000..9fac05738c --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/Models.Customer.fr-FR.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour from Customer in resources folder + + \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/StartupCustomCulturePreserved.en-US.resx b/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/StartupCustomCulturePreserved.en-US.resx new file mode 100644 index 0000000000..e163149384 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/StartupCustomCulturePreserved.en-US.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + We shouldn't get the english hello! + + \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/StartupResourcesInFolder.fr-FR.resx b/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/StartupResourcesInFolder.fr-FR.resx new file mode 100644 index 0000000000..04d53a1b2e --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/StartupResourcesInFolder.fr-FR.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour from StartupResourcesInFolder + + \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/Test.fr-FR.resx b/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/Test.fr-FR.resx new file mode 100644 index 0000000000..31789167b1 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Resources/Test.fr-FR.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour from Test in resources folder + + \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupBuilderAPIs.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupBuilderAPIs.cs new file mode 100644 index 0000000000..4dd4ec41e3 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupBuilderAPIs.cs @@ -0,0 +1,42 @@ +// 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 LocalizationWebsite.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +namespace LocalizationWebsite +{ + public class StartupBuilderAPIs + { + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(options => options.ResourcesPath = "Resources"); + } + + public void Configure( + IApplicationBuilder app, + ILoggerFactory loggerFactory, + IStringLocalizer customerStringLocalizer) + { + var supportedCultures = new[] { "en-US", "fr-FR" }; + app.UseRequestLocalization(options => + options + .AddSupportedCultures(supportedCultures) + .AddSupportedUICultures(supportedCultures) + .SetDefaultCulture("ar-YE") + ); + + app.Run(async (context) => + { + var requestCultureFeature = context.Features.Get(); + var requestCulture = requestCultureFeature.RequestCulture; + await context.Response.WriteAsync(customerStringLocalizer["Hello"]); + }); + } + } +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupCustomCulturePreserved.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupCustomCulturePreserved.cs new file mode 100644 index 0000000000..778daa1387 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupCustomCulturePreserved.cs @@ -0,0 +1,42 @@ +// 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 System.Globalization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.DependencyInjection; + +namespace LocalizationWebsite +{ + public class StartupCustomCulturePreserved + { + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(); + } + + public void Configure( + IApplicationBuilder app) + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List() + { + new CultureInfo("en-US") { NumberFormat= { CurrencySymbol = "kr" } } + }, + SupportedUICultures = new List() + { + new CultureInfo("en-US") { NumberFormat= { CurrencySymbol = "kr" } } + } + }); + + app.Run(async (context) => + { + await context.Response.WriteAsync(10.ToString("C")); + }); + } + } +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupGetAllStrings.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupGetAllStrings.cs new file mode 100644 index 0000000000..730a745d07 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupGetAllStrings.cs @@ -0,0 +1,52 @@ +// 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 System.Globalization; +using System.Linq; +using LocalizationWebsite.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +namespace LocalizationWebsite +{ + public class StartupGetAllStrings + { + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(options => options.ResourcesPath = "Resources"); + } + + public void Configure( + IApplicationBuilder app, + ILoggerFactory loggerFactory, + IStringLocalizer customerStringLocalizer) + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List() + { + new CultureInfo("fr-FR") + }, + SupportedUICultures = new List() + { + new CultureInfo("fr-FR") + } + }); + + app.Run(async (context) => + { + var strings = customerStringLocalizer.GetAllStrings(); + + await context.Response.WriteAsync(strings.Count().ToString()); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(string.Join(" ", strings)); + }); + } + } +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesAtRootFolder.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesAtRootFolder.cs new file mode 100644 index 0000000000..2d54c02c33 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesAtRootFolder.cs @@ -0,0 +1,57 @@ +// 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 System.Globalization; +using System.Reflection; +using LocalizationWebsite.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +namespace LocalizationWebsite +{ + public class StartupResourcesAtRootFolder + { + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(); + } + + public void Configure( + IApplicationBuilder app, + ILoggerFactory loggerFactory, + IStringLocalizerFactory stringLocalizerFactory, + IStringLocalizer startupStringLocalizer, + IStringLocalizer customerStringLocalizer) + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List() + { + new CultureInfo("fr-FR") + }, + SupportedUICultures = new List() + { + new CultureInfo("fr-FR") + } + }); + + var location = typeof(LocalizationWebsite.StartupResourcesAtRootFolder).GetTypeInfo().Assembly.GetName().Name; + var stringLocalizer = stringLocalizerFactory.Create("Test", location: location); + + app.Run(async (context) => + { + await context.Response.WriteAsync(startupStringLocalizer["Hello"]); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(stringLocalizer["Hello"]); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(customerStringLocalizer["Hello"]); + }); + } + } +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesAtRootFolder.fr-FR.resx b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesAtRootFolder.fr-FR.resx new file mode 100644 index 0000000000..42b36579eb --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesAtRootFolder.fr-FR.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour from StartupResourcesAtRootFolder + + \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInClassLibrary.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInClassLibrary.cs new file mode 100644 index 0000000000..092d09a37b --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInClassLibrary.cs @@ -0,0 +1,68 @@ +// 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 System.Globalization; +using System.Reflection; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +namespace LocalizationWebsite +{ + public class StartupResourcesInClassLibrary + { + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(options => options.ResourcesPath = "Resources"); + } + + public void Configure( + IApplicationBuilder app, + ILoggerFactory loggerFactory, + IStringLocalizerFactory stringLocalizerFactory) + { + var supportedCultures = new List() + { + new CultureInfo("en-US"), + new CultureInfo("fr-FR") + }; + + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = supportedCultures, + SupportedUICultures = supportedCultures + }); + + var noAttributeStringLocalizer = stringLocalizerFactory.Create(typeof(ResourcesClassLibraryNoAttribute.Model)); + var withAttributeStringLocalizer = stringLocalizerFactory.Create(typeof(ResourcesClassLibraryWithAttribute.Model)); + + var noAttributeAssembly = typeof(ResourcesClassLibraryNoAttribute.Model).GetTypeInfo().Assembly; + var noAttributeName = new AssemblyName(noAttributeAssembly.FullName).Name; + var noAttributeNameStringLocalizer = stringLocalizerFactory.Create( + nameof(ResourcesClassLibraryNoAttribute.Model), + noAttributeName); + + var withAttributeAssembly = typeof(ResourcesClassLibraryWithAttribute.Model).GetTypeInfo().Assembly; + var withAttributeName = new AssemblyName(withAttributeAssembly.FullName).Name; + var withAttributeNameStringLocalizer = stringLocalizerFactory.Create( + nameof(ResourcesClassLibraryWithAttribute.Model), + withAttributeName); + + app.Run(async (context) => + { + await context.Response.WriteAsync(noAttributeNameStringLocalizer["Hello"]); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(noAttributeStringLocalizer["Hello"]); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(withAttributeNameStringLocalizer["Hello"]); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(withAttributeStringLocalizer["Hello"]); + }); + } + } +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInFolder.cs b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInFolder.cs new file mode 100644 index 0000000000..d4c4f9e028 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/StartupResourcesInFolder.cs @@ -0,0 +1,62 @@ +// 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 System.Globalization; +using System.Reflection; +using LocalizationWebsite.Models; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Localization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Logging; + +namespace LocalizationWebsite +{ + public class StartupResourcesInFolder + { + public void ConfigureServices(IServiceCollection services) + { + services.AddLocalization(options => options.ResourcesPath = "Resources"); + } + + public void Configure( + IApplicationBuilder app, + ILoggerFactory loggerFactory, + IStringLocalizerFactory stringLocalizerFactory, + IStringLocalizer startupStringLocalizer, + IStringLocalizer custromerStringLocalizer, + // This localizer is used in tests to prevent a regression of https://github.com/aspnet/Localization/issues/293 + // Namely that english was always being returned if it existed. + IStringLocalizer customCultureLocalizer) + { + app.UseRequestLocalization(new RequestLocalizationOptions + { + DefaultRequestCulture = new RequestCulture("en-US"), + SupportedCultures = new List() + { + new CultureInfo("fr-FR") + }, + SupportedUICultures = new List() + { + new CultureInfo("fr-FR") + } + }); + + var assemblyName = typeof(StartupResourcesInFolder).GetTypeInfo().Assembly.GetName().Name; + var stringLocalizer = stringLocalizerFactory.Create("Test", assemblyName); + + app.Run(async (context) => + { + await context.Response.WriteAsync(startupStringLocalizer["Hello"]); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(stringLocalizer["Hello"]); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(custromerStringLocalizer["Hello"]); + await context.Response.WriteAsync(" "); + await context.Response.WriteAsync(customCultureLocalizer["Hello"]); + }); + } + } +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Test.fr-FR.resx b/src/Middleware/Localization/testassets/LocalizationWebsite/Test.fr-FR.resx new file mode 100644 index 0000000000..08f4fc8656 --- /dev/null +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Test.fr-FR.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour from Test in root folder + + \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/ResourcesClassLibraryNoAttribute/Model.cs b/src/Middleware/Localization/testassets/ResourcesClassLibraryNoAttribute/Model.cs new file mode 100644 index 0000000000..9921388c89 --- /dev/null +++ b/src/Middleware/Localization/testassets/ResourcesClassLibraryNoAttribute/Model.cs @@ -0,0 +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. + +namespace ResourcesClassLibraryNoAttribute +{ + public class Model + { + } +} diff --git a/src/Middleware/Localization/testassets/ResourcesClassLibraryNoAttribute/Resources/Model.resx b/src/Middleware/Localization/testassets/ResourcesClassLibraryNoAttribute/Resources/Model.resx new file mode 100644 index 0000000000..af89e6e6ee --- /dev/null +++ b/src/Middleware/Localization/testassets/ResourcesClassLibraryNoAttribute/Resources/Model.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour from ResourcesClassLibraryNoAttribute + + \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/ResourcesClassLibraryNoAttribute/ResourcesClassLibraryNoAttribute.csproj b/src/Middleware/Localization/testassets/ResourcesClassLibraryNoAttribute/ResourcesClassLibraryNoAttribute.csproj new file mode 100644 index 0000000000..dbdcea46b6 --- /dev/null +++ b/src/Middleware/Localization/testassets/ResourcesClassLibraryNoAttribute/ResourcesClassLibraryNoAttribute.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.0 + + + diff --git a/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/AssemblyInfo.cs b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/AssemblyInfo.cs new file mode 100644 index 0000000000..bf01e53ef0 --- /dev/null +++ b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// 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.Reflection; +using Microsoft.Extensions.Localization; + +[assembly: ResourceLocation("ResourceFolder")] +[assembly: RootNamespace("Alternate.Namespace")] \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/Model.cs b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/Model.cs new file mode 100644 index 0000000000..c6ca99afa8 --- /dev/null +++ b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/Model.cs @@ -0,0 +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. + +namespace ResourcesClassLibraryWithAttribute +{ + public class Model + { + } +} diff --git a/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/ResourceFolder/Model.resx b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/ResourceFolder/Model.resx new file mode 100644 index 0000000000..1b10f56f23 --- /dev/null +++ b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/ResourceFolder/Model.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Bonjour from ResourcesClassLibraryWithAttribute + + \ No newline at end of file diff --git a/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/ResourcesClassLibraryWithAttribute.csproj b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/ResourcesClassLibraryWithAttribute.csproj new file mode 100644 index 0000000000..9d1523c3be --- /dev/null +++ b/src/Middleware/Localization/testassets/ResourcesClassLibraryWithAttribute/ResourcesClassLibraryWithAttribute.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + Alternate.Namespace + + + + + + +