diff --git a/Localization.sln b/Localization.sln index ddfd06056ca0..cf7a3d95c67c 100644 --- a/Localization.sln +++ b/Localization.sln @@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution global.json = global.json EndProjectSection EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "CultureInfoGenerator", "src\CultureInfoGenerator\CultureInfoGenerator.xproj", "{BD22AE1C-6631-4DA6-874D-0DC0F803CEAB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -42,6 +44,10 @@ Global {55D9501F-15B9-4339-A0AB-6082850E5FCE}.Debug|Any CPU.Build.0 = Debug|Any CPU {55D9501F-15B9-4339-A0AB-6082850E5FCE}.Release|Any CPU.ActiveCfg = Release|Any CPU {55D9501F-15B9-4339-A0AB-6082850E5FCE}.Release|Any CPU.Build.0 = Release|Any CPU + {BD22AE1C-6631-4DA6-874D-0DC0F803CEAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BD22AE1C-6631-4DA6-874D-0DC0F803CEAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BD22AE1C-6631-4DA6-874D-0DC0F803CEAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BD22AE1C-6631-4DA6-874D-0DC0F803CEAB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -51,5 +57,6 @@ Global {23E3BC23-3464-4D9B-BF78-02CB2182BEF0} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767} {A1FCF259-70F6-4605-AA2D-E4B356BE771A} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767} {55D9501F-15B9-4339-A0AB-6082850E5FCE} = {79878809-8D1C-4BD4-BA99-F1F13FF96FD8} + {BD22AE1C-6631-4DA6-874D-0DC0F803CEAB} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767} EndGlobalSection EndGlobal diff --git a/samples/LocalizationSample/Startup.cs b/samples/LocalizationSample/Startup.cs index d819c939992a..2183eaf3afd3 100644 --- a/samples/LocalizationSample/Startup.cs +++ b/samples/LocalizationSample/Startup.cs @@ -123,7 +123,8 @@ private static async System.Threading.Tasks.Task WriteCultureSelectOptions(HttpC #if DNX451 await context.Response.WriteAsync($" "); #endif - await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); + await context.Response.WriteAsync($" "); } } } diff --git a/src/CultureInfoGenerator/CultureInfoGenerator.xproj b/src/CultureInfoGenerator/CultureInfoGenerator.xproj new file mode 100644 index 000000000000..6e980d08c1d2 --- /dev/null +++ b/src/CultureInfoGenerator/CultureInfoGenerator.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + bd22ae1c-6631-4da6-874d-0dc0f803ceab + CultureInfoGenerator + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/CultureInfoGenerator/Program.cs b/src/CultureInfoGenerator/Program.cs new file mode 100644 index 000000000000..7843c928d7ee --- /dev/null +++ b/src/CultureInfoGenerator/Program.cs @@ -0,0 +1,116 @@ +// 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.IO; +using Microsoft.Framework.Runtime; +using Microsoft.Win32; + +namespace CultureInfoGenerator +{ + public class Program + { + private readonly string _appName; + private readonly string _appPath; + + public Program(IApplicationEnvironment appEnvironment) + { + _appName = appEnvironment.ApplicationName; + _appPath = appEnvironment.ApplicationBasePath; + } + + public void Main(string[] args) + { + var outputFilePath = args.Length > 0 ? args[0] : Path.Combine(_appPath, "../Microsoft.AspNet.Localization/Internal/CultureInfoList.cs"); + var netFxVersion = Get45or451FromRegistry(); + var windowsVersion = Environment.OSVersion; + + using (var writer = new StreamWriter(outputFilePath, false)) + { + writer.WriteLine($@"// 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; + +namespace Microsoft.AspNet.Localization.Internal +{{ + public static class CultureInfoList + {{ + // This list of known cultures was generated by {_appName} using .NET Framework {netFxVersion} on + // {windowsVersion}. + // As new versions of .NET Framework and Windows are released, this list should be regenerated to ensure it + // contains the latest culture names. + public static readonly HashSet KnownCultureNames = new HashSet + {{" + ); + + var cultures = CultureInfo.GetCultures( + CultureTypes.NeutralCultures + | CultureTypes.InstalledWin32Cultures + | CultureTypes.SpecificCultures); + + var format = " \"{0}\""; + + for (int i = 0; i < cultures.Length; i++) + { + var culture = cultures[i]; + + writer.Write(format, culture.Name); + + if (i < cultures.Length - 1) + { + writer.WriteLine(","); + } + else + { + // Last entry + writer.WriteLine(); + } + } + + writer.WriteLine( +@" }; + } +}"); + } + } + + // .NET Framework detection code copied from https://msdn.microsoft.com/en-us/library/hh925568%28v=vs.110%29.aspx#net_d + private static string Get45or451FromRegistry() + { + using (var ndpKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32) + .OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full\\")) + { + var releaseKey = Convert.ToInt32(ndpKey.GetValue("Release")); + return CheckFor45DotVersion(releaseKey); + } + } + + // Checking the version using >= will enable forward compatibility, + // however you should always compile your code on newer versions of + // the framework to ensure your app works the same. + private static string CheckFor45DotVersion(int releaseKey) + { + if (releaseKey >= 393273) + { + return "4.6 RC or later"; + } + if ((releaseKey >= 379893)) + { + return "4.5.2 or later"; + } + if ((releaseKey >= 378675)) + { + return "4.5.1 or later"; + } + if ((releaseKey >= 378389)) + { + return "4.5 or later"; + } + // This line should never execute. A non-null release key should mean + // that 4.5 or later is installed. + return "No 4.5 or later version detected"; + } + } +} diff --git a/src/CultureInfoGenerator/project.json b/src/CultureInfoGenerator/project.json new file mode 100644 index 000000000000..f88d824517af --- /dev/null +++ b/src/CultureInfoGenerator/project.json @@ -0,0 +1,16 @@ +{ + "version": "1.0.0-*", + "description": "Generates a list of known culture names from the OS using CultureInfo.GetCultures. This tool is intended to be run on Windows using full .NET Framework.", + + "dependencies": { + "Microsoft.Framework.Runtime.Abstractions": "1.0.0-beta5-11739" + }, + + "commands": { + "CultureInfoGenerator": "CultureInfoGenerator" + }, + + "frameworks": { + "dnx451": { } + } +} diff --git a/src/Microsoft.AspNet.Localization/Internal/CultureInfoCache.cs b/src/Microsoft.AspNet.Localization/Internal/CultureInfoCache.cs index 87662bf84058..9e8ee0a9930e 100644 --- a/src/Microsoft.AspNet.Localization/Internal/CultureInfoCache.cs +++ b/src/Microsoft.AspNet.Localization/Internal/CultureInfoCache.cs @@ -11,11 +11,11 @@ public static class CultureInfoCache { private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); - public static CultureInfo GetCultureInfo(string name, bool throwIfNotFound = false) + public static CultureInfo GetCultureInfo(string name) { - // Allow empty string values as they map to InvariantCulture, whereas null culture values will throw in - // the CultureInfo ctor - if (name == null) + // 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 || !CultureInfoList.KnownCultureNames.Contains(name)) { return null; } @@ -26,17 +26,15 @@ public static CultureInfo GetCultureInfo(string name, bool throwIfNotFound = fal { return new CacheEntry(CultureInfo.ReadOnly(new CultureInfo(n))); } - catch (CultureNotFoundException ex) + catch (CultureNotFoundException) { - return new CacheEntry(ex); + // This can still throw as the list of culture names we have is generated from latest .NET Framework + // on latest Windows and thus contains names that won't be supported on lower framework or OS versions. + // We can just cache the null result in these cases as it's ultimately bound by the list anyway. + return new CacheEntry(cultureInfo: null); } }); - if (entry.Exception != null && throwIfNotFound) - { - throw entry.Exception; - } - return entry.CultureInfo; } @@ -47,14 +45,7 @@ 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/CultureInfoList.cs b/src/Microsoft.AspNet.Localization/Internal/CultureInfoList.cs new file mode 100644 index 000000000000..1d178738eb6e --- /dev/null +++ b/src/Microsoft.AspNet.Localization/Internal/CultureInfoList.cs @@ -0,0 +1,436 @@ +// 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; + +namespace Microsoft.AspNet.Localization.Internal +{ + public static class CultureInfoList + { + // This list of known cultures was generated by CultureInfoGenerator using .NET Framework 4.6 RC or later on + // Microsoft Windows NT 6.2.9200.0. + // As new versions of .NET Framework and Windows are released, this list should be regenerated to ensure it + // contains the latest culture names. + public static readonly HashSet KnownCultureNames = new HashSet + { + "", + "af", + "af-ZA", + "am", + "am-ET", + "ar", + "ar-AE", + "ar-BH", + "ar-DZ", + "ar-EG", + "ar-IQ", + "ar-JO", + "ar-KW", + "ar-LB", + "ar-LY", + "ar-MA", + "ar-OM", + "ar-QA", + "ar-SA", + "ar-SY", + "ar-TN", + "ar-YE", + "arn", + "arn-CL", + "as", + "as-IN", + "az", + "az-Cyrl", + "az-Cyrl-AZ", + "az-Latn", + "az-Latn-AZ", + "ba", + "ba-RU", + "be", + "be-BY", + "bg", + "bg-BG", + "bn", + "bn-BD", + "bn-IN", + "bo", + "bo-CN", + "br", + "br-FR", + "bs", + "bs-Cyrl", + "bs-Cyrl-BA", + "bs-Latn", + "bs-Latn-BA", + "ca", + "ca-ES", + "ca-ES-valencia", + "chr", + "chr-Cher", + "chr-Cher-US", + "co", + "co-FR", + "cs", + "cs-CZ", + "cy", + "cy-GB", + "da", + "da-DK", + "de", + "de-AT", + "de-CH", + "de-DE", + "de-LI", + "de-LU", + "dsb", + "dsb-DE", + "dv", + "dv-MV", + "el", + "el-GR", + "en", + "en-029", + "en-AU", + "en-BZ", + "en-CA", + "en-GB", + "en-HK", + "en-IE", + "en-IN", + "en-JM", + "en-MY", + "en-NZ", + "en-PH", + "en-SG", + "en-TT", + "en-US", + "en-ZA", + "en-ZW", + "es", + "es-419", + "es-AR", + "es-BO", + "es-CL", + "es-CO", + "es-CR", + "es-DO", + "es-EC", + "es-ES", + "es-GT", + "es-HN", + "es-MX", + "es-NI", + "es-PA", + "es-PE", + "es-PR", + "es-PY", + "es-SV", + "es-US", + "es-UY", + "es-VE", + "et", + "et-EE", + "eu", + "eu-ES", + "fa", + "fa-IR", + "ff", + "ff-Latn", + "ff-Latn-SN", + "fi", + "fi-FI", + "fil", + "fil-PH", + "fo", + "fo-FO", + "fr", + "fr-BE", + "fr-CA", + "fr-CD", + "fr-CH", + "fr-CI", + "fr-CM", + "fr-FR", + "fr-HT", + "fr-LU", + "fr-MA", + "fr-MC", + "fr-ML", + "fr-RE", + "fr-SN", + "fy", + "fy-NL", + "ga", + "ga-IE", + "gd", + "gd-GB", + "gl", + "gl-ES", + "gn", + "gn-PY", + "gsw", + "gsw-FR", + "gu", + "gu-IN", + "ha", + "ha-Latn", + "ha-Latn-NG", + "haw", + "haw-US", + "he", + "he-IL", + "hi", + "hi-IN", + "hr", + "hr-BA", + "hr-HR", + "hsb", + "hsb-DE", + "hu", + "hu-HU", + "hy", + "hy-AM", + "id", + "id-ID", + "ig", + "ig-NG", + "ii", + "ii-CN", + "is", + "is-IS", + "it", + "it-CH", + "it-IT", + "iu", + "iu-Cans", + "iu-Cans-CA", + "iu-Latn", + "iu-Latn-CA", + "ja", + "ja-JP", + "jv", + "jv-Latn", + "jv-Latn-ID", + "ka", + "ka-GE", + "kk", + "kk-KZ", + "kl", + "kl-GL", + "km", + "km-KH", + "kn", + "kn-IN", + "ko", + "ko-KR", + "kok", + "kok-IN", + "ku", + "ku-Arab", + "ku-Arab-IQ", + "ky", + "ky-KG", + "lb", + "lb-LU", + "lo", + "lo-LA", + "lt", + "lt-LT", + "lv", + "lv-LV", + "mg", + "mg-MG", + "mi", + "mi-NZ", + "mk", + "mk-MK", + "ml", + "ml-IN", + "mn", + "mn-Cyrl", + "mn-MN", + "mn-Mong", + "mn-Mong-CN", + "mn-Mong-MN", + "moh", + "moh-CA", + "mr", + "mr-IN", + "ms", + "ms-BN", + "ms-MY", + "mt", + "mt-MT", + "my", + "my-MM", + "nb", + "nb-NO", + "ne", + "ne-IN", + "ne-NP", + "nl", + "nl-BE", + "nl-NL", + "nn", + "nn-NO", + "no", + "nqo", + "nqo-GN", + "nso", + "nso-ZA", + "oc", + "oc-FR", + "om", + "om-ET", + "or", + "or-IN", + "pa", + "pa-Arab", + "pa-Arab-PK", + "pa-IN", + "pl", + "pl-PL", + "prs", + "prs-AF", + "ps", + "ps-AF", + "pt", + "pt-AO", + "pt-BR", + "pt-PT", + "qut", + "qut-GT", + "quz", + "quz-BO", + "quz-EC", + "quz-PE", + "rm", + "rm-CH", + "ro", + "ro-MD", + "ro-RO", + "ru", + "ru-RU", + "rw", + "rw-RW", + "sa", + "sa-IN", + "sah", + "sah-RU", + "sd", + "sd-Arab", + "sd-Arab-PK", + "se", + "se-FI", + "se-NO", + "se-SE", + "si", + "si-LK", + "sk", + "sk-SK", + "sl", + "sl-SI", + "sma", + "sma-NO", + "sma-SE", + "smj", + "smj-NO", + "smj-SE", + "smn", + "smn-FI", + "sms", + "sms-FI", + "sn", + "sn-Latn", + "sn-Latn-ZW", + "so", + "so-SO", + "sq", + "sq-AL", + "sr", + "sr-Cyrl", + "sr-Cyrl-BA", + "sr-Cyrl-CS", + "sr-Cyrl-ME", + "sr-Cyrl-RS", + "sr-Latn", + "sr-Latn-BA", + "sr-Latn-CS", + "sr-Latn-ME", + "sr-Latn-RS", + "st", + "st-ZA", + "sv", + "sv-FI", + "sv-SE", + "sw", + "sw-KE", + "syr", + "syr-SY", + "ta", + "ta-IN", + "ta-LK", + "te", + "te-IN", + "tg", + "tg-Cyrl", + "tg-Cyrl-TJ", + "th", + "th-TH", + "ti", + "ti-ER", + "ti-ET", + "tk", + "tk-TM", + "tn", + "tn-BW", + "tn-ZA", + "tr", + "tr-TR", + "ts", + "ts-ZA", + "tt", + "tt-RU", + "tzm", + "tzm-Latn", + "tzm-Latn-DZ", + "tzm-Tfng", + "tzm-Tfng-MA", + "ug", + "ug-CN", + "uk", + "uk-UA", + "ur", + "ur-IN", + "ur-PK", + "uz", + "uz-Cyrl", + "uz-Cyrl-UZ", + "uz-Latn", + "uz-Latn-UZ", + "vi", + "vi-VN", + "wo", + "wo-SN", + "xh", + "xh-ZA", + "yo", + "yo-NG", + "zgh", + "zgh-Tfng", + "zgh-Tfng-MA", + "zh", + "zh-CN", + "zh-Hans", + "zh-Hant", + "zh-HK", + "zh-MO", + "zh-SG", + "zh-TW", + "zu", + "zu-ZA", + "zh-CHS", + "zh-CHT" + }; + } +}