diff --git a/.gitignore b/.gitignore index 65df2761c475..aba84927ed9c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ [Bb]in/ TestResults/ .nuget/ +.vs/ _ReSharper.*/ packages/ artifacts/ @@ -25,4 +26,4 @@ nuget.exe *.ipch *.sln.ide debugSettings.json -project.lock.json +project.lock.json \ No newline at end of file diff --git a/samples/LocalizationSample/Startup.cs b/samples/LocalizationSample/Startup.cs index dc7fb81799e5..d819c939992a 100644 --- a/samples/LocalizationSample/Startup.cs +++ b/samples/LocalizationSample/Startup.cs @@ -27,8 +27,15 @@ public void Configure(IApplicationBuilder app, IStringLocalizer SR) }; app.UseRequestLocalization(options); - app.Run(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"; @@ -39,12 +46,25 @@ await context.Response.WriteAsync( $@" - Request Localization + {SR["Request Localization"]} + "); await context.Response.WriteAsync($"

{SR["Request Localization Sample"]}

"); @@ -57,8 +77,9 @@ 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($"{SR["reset"]}"); await context.Response.WriteAsync(""); await context.Response.WriteAsync("
"); await context.Response.WriteAsync(""); @@ -102,6 +123,7 @@ private static async System.Threading.Tasks.Task WriteCultureSelectOptions(HttpC #if DNX451 await context.Response.WriteAsync($" "); #endif + await context.Response.WriteAsync($" "); } } } diff --git a/src/Microsoft.AspNet.Localization/AcceptLanguageHeaderRequestCultureStrategy.cs b/src/Microsoft.AspNet.Localization/AcceptLanguageHeaderRequestCultureStrategy.cs index 1a90e5332a78..030e29436177 100644 --- a/src/Microsoft.AspNet.Localization/AcceptLanguageHeaderRequestCultureStrategy.cs +++ b/src/Microsoft.AspNet.Localization/AcceptLanguageHeaderRequestCultureStrategy.cs @@ -13,9 +13,6 @@ namespace Microsoft.AspNet.Localization /// /// Determines the culture information for a request via the value of the Accept-Language header. /// - /// - /// - /// public class AcceptLanguageHeaderRequestCultureStrategy : IRequestCultureStrategy { /// @@ -53,11 +50,11 @@ public RequestCulture DetermineRequestCulture([NotNull] HttpContext httpContext) // the CultureInfo ctor if (language.Value != null) { - try + var culture = CultureInfoCache.GetCultureInfo(language.Value); + if (culture != null) { - return new RequestCulture(CultureUtilities.GetCultureFromName(language.Value)); + return RequestCulture.GetRequestCulture(culture); } - catch (CultureNotFoundException) { } } } diff --git a/src/Microsoft.AspNet.Localization/CookieRequestCultureStrategy.cs b/src/Microsoft.AspNet.Localization/CookieRequestCultureStrategy.cs index 902d5819dd53..ac9a1b30894c 100644 --- a/src/Microsoft.AspNet.Localization/CookieRequestCultureStrategy.cs +++ b/src/Microsoft.AspNet.Localization/CookieRequestCultureStrategy.cs @@ -3,16 +3,98 @@ using System; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Localization.Internal; using Microsoft.Framework.Internal; namespace Microsoft.AspNet.Localization { + /// + /// Determines the culture information for a request via the value of a cookie. + /// public class CookieRequestCultureStrategy : IRequestCultureStrategy { + private static readonly char[] _cookieSeparator = new[] { '|' }; + private static readonly string _culturePrefix = "c="; + private static readonly string _uiCulturePrefix = "uic="; + + /// + /// The name of the cookie that contains the user's preferred culture information. + /// Defaults to . + /// + public string CookieName { get; set; } = DefaultCookieName; + + /// public RequestCulture DetermineRequestCulture([NotNull] HttpContext httpContext) { - // TODO - return null; + var cookie = httpContext.Request.Cookies[CookieName]; + + if (cookie == null) + { + return null; + } + + return ParseCookieValue(cookie); + } + + /// + /// The default name of the cookie used to track the user's preferred culture information. + /// + public static string DefaultCookieName { get; } = "ASPNET_CULTURE"; + + /// + /// Creates a string representation of a for placement in a cookie. + /// + /// The . + /// The cookie value. + public static string MakeCookieValue([NotNull] RequestCulture 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 RequestCulture ParseCookieValue([NotNull] 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); + + var culture = CultureInfoCache.GetCultureInfo(cultureName); + var uiCulture = CultureInfoCache.GetCultureInfo(uiCultureName); + + if (culture == null || uiCulture == null) + { + return null; + } + + return RequestCulture.GetRequestCulture(culture, uiCulture); } } } diff --git a/src/Microsoft.AspNet.Localization/Internal/CultureInfoCache.cs b/src/Microsoft.AspNet.Localization/Internal/CultureInfoCache.cs new file mode 100644 index 000000000000..87662bf84058 --- /dev/null +++ b/src/Microsoft.AspNet.Localization/Internal/CultureInfoCache.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.Collections.Concurrent; +using System.Globalization; + +namespace Microsoft.AspNet.Localization.Internal +{ + public static class CultureInfoCache + { + private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + + public static CultureInfo GetCultureInfo(string name, bool throwIfNotFound = false) + { + // Allow empty string values as they map to InvariantCulture, whereas null culture values will throw in + // the CultureInfo ctor + if (name == null) + { + return null; + } + + var entry = _cache.GetOrAdd(name, n => + { + try + { + return new CacheEntry(CultureInfo.ReadOnly(new CultureInfo(n))); + } + catch (CultureNotFoundException ex) + { + return new CacheEntry(ex); + } + }); + + if (entry.Exception != null && throwIfNotFound) + { + throw entry.Exception; + } + + return entry.CultureInfo; + } + + private class CacheEntry + { + public CacheEntry(CultureInfo cultureInfo) + { + CultureInfo = cultureInfo; + } + + public CacheEntry(Exception exception) + { + Exception = exception; + } + + public CultureInfo CultureInfo { get; } + + public Exception Exception { get; } + } + } +} diff --git a/src/Microsoft.AspNet.Localization/Internal/CultureUtilities.cs b/src/Microsoft.AspNet.Localization/Internal/CultureUtilities.cs deleted file mode 100644 index bea6d2145df8..000000000000 --- a/src/Microsoft.AspNet.Localization/Internal/CultureUtilities.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Globalization; - -namespace Microsoft.AspNet.Localization.Internal -{ - public static class CultureUtilities - { - public static CultureInfo GetCultureFromName(string cultureName) - { - // Allow empty string values as they map to InvariantCulture, whereas null culture values will throw in - // the CultureInfo ctor - if (cultureName == null) - { - return null; - } - - try - { - return new CultureInfo(cultureName); - } - catch (CultureNotFoundException) - { - return null; - } - } - } -} diff --git a/src/Microsoft.AspNet.Localization/QueryStringRequestCultureStrategy.cs b/src/Microsoft.AspNet.Localization/QueryStringRequestCultureStrategy.cs index c7cfdb77e74e..80a7ba7f1fd5 100644 --- a/src/Microsoft.AspNet.Localization/QueryStringRequestCultureStrategy.cs +++ b/src/Microsoft.AspNet.Localization/QueryStringRequestCultureStrategy.cs @@ -59,9 +59,15 @@ public RequestCulture DetermineRequestCulture([NotNull] HttpContext httpContext) queryUICulture = queryCulture; } - return new RequestCulture( - CultureUtilities.GetCultureFromName(queryCulture), - CultureUtilities.GetCultureFromName(queryUICulture)); + var culture = CultureInfoCache.GetCultureInfo(queryCulture); + var uiCulture = CultureInfoCache.GetCultureInfo(queryUICulture); + + if (culture == null || uiCulture == null) + { + return null; + } + + return RequestCulture.GetRequestCulture(culture, uiCulture); } } } diff --git a/src/Microsoft.AspNet.Localization/RequestCulture.cs b/src/Microsoft.AspNet.Localization/RequestCulture.cs index ec109e6d8ee5..9f0e2754db22 100644 --- a/src/Microsoft.AspNet.Localization/RequestCulture.cs +++ b/src/Microsoft.AspNet.Localization/RequestCulture.cs @@ -1,6 +1,7 @@ // 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.Concurrent; using System.Globalization; using Microsoft.Framework.Internal; @@ -11,24 +12,15 @@ namespace Microsoft.AspNet.Localization /// public class RequestCulture { - /// - /// Creates a new object has its and - /// properties set to the same value. - /// - /// The for the request. - public RequestCulture([NotNull] CultureInfo culture) + private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + + private RequestCulture([NotNull] CultureInfo culture) : this (culture, culture) { } - /// - /// 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([NotNull] CultureInfo culture, [NotNull] CultureInfo uiCulture) + private RequestCulture([NotNull] CultureInfo culture, [NotNull] CultureInfo uiCulture) { Culture = culture; UICulture = uiCulture; @@ -43,5 +35,66 @@ public RequestCulture([NotNull] CultureInfo culture, [NotNull] CultureInfo uiCul /// Gets the for the request to be used for text, i.e. language; /// public CultureInfo UICulture { get; } + + /// + /// Gets a cached instance that has its and + /// properties set to the same value. + /// + /// The for the request. + public static RequestCulture GetRequestCulture([NotNull] CultureInfo culture) + { + return GetRequestCulture(culture, culture); + } + + /// + /// Gets a cached instance that 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 static RequestCulture GetRequestCulture([NotNull] CultureInfo culture, [NotNull] CultureInfo uiCulture) + { + var key = new CacheKey(culture, uiCulture); + return _cache.GetOrAdd(key, k => new RequestCulture(culture, uiCulture)); + } + + private class CacheKey + { + private readonly int _hashCode; + + public CacheKey(CultureInfo culture, CultureInfo uiCulture) + { + Culture = culture; + UICulture = uiCulture; + _hashCode = new { Culture, UICulture }.GetHashCode(); + } + + public CultureInfo Culture { get; } + + public CultureInfo UICulture { get; } + + public bool Equals(CacheKey other) + { + return Culture == other.Culture && UICulture == other.UICulture; + } + + public override bool Equals(object obj) + { + var other = obj as CacheKey; + + if (other != null) + { + return Equals(other); + } + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return _hashCode; + } + } } } diff --git a/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs b/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs index 43ac3496dc14..1cd4ea2be1cc 100644 --- a/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs +++ b/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs @@ -38,7 +38,7 @@ public RequestLocalizationMiddleware([NotNull] RequestDelegate next, [NotNull] R public async Task Invoke([NotNull] HttpContext context) { var requestCulture = _options.DefaultRequestCulture ?? - new RequestCulture(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture); + RequestCulture.GetRequestCulture(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture); IRequestCultureStrategy winningStrategy = null; @@ -61,12 +61,12 @@ public async Task Invoke([NotNull] HttpContext context) // Ensure that selected cultures are in the supported list and if not, set them to the default if (!_options.SupportedCultures.Contains(requestCulture.Culture)) { - requestCulture = new RequestCulture(_options.DefaultRequestCulture.Culture, requestCulture.UICulture); + requestCulture = RequestCulture.GetRequestCulture(_options.DefaultRequestCulture.Culture, requestCulture.UICulture); } if (!_options.SupportedUICultures.Contains(requestCulture.UICulture)) { - requestCulture = new RequestCulture(requestCulture.Culture, _options.DefaultRequestCulture.UICulture); + requestCulture = RequestCulture.GetRequestCulture(requestCulture.Culture, _options.DefaultRequestCulture.UICulture); } } diff --git a/src/Microsoft.AspNet.Localization/RequestLocalizationMiddlewareOptions.cs b/src/Microsoft.AspNet.Localization/RequestLocalizationMiddlewareOptions.cs index 32827ebb1afb..0f24d18cf79d 100644 --- a/src/Microsoft.AspNet.Localization/RequestLocalizationMiddlewareOptions.cs +++ b/src/Microsoft.AspNet.Localization/RequestLocalizationMiddlewareOptions.cs @@ -17,7 +17,7 @@ public class RequestLocalizationMiddlewareOptions /// public RequestLocalizationMiddlewareOptions() { - DefaultRequestCulture = new RequestCulture(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture); + DefaultRequestCulture = RequestCulture.GetRequestCulture(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture); RequestCultureStrategies = new List { diff --git a/src/Microsoft.AspNet.Localization/project.json b/src/Microsoft.AspNet.Localization/project.json index a6d3910968c1..069295bd000c 100644 --- a/src/Microsoft.AspNet.Localization/project.json +++ b/src/Microsoft.AspNet.Localization/project.json @@ -13,6 +13,7 @@ "dnxcore50": { "dependencies": { "System.Collections": "4.0.10-*", + "System.Collections.Concurrent": "4.0.10-*", "System.Linq": "4.0.0-*", "System.Globalization": "4.0.10-*", "System.Threading": "4.0.10-*",