commit 9eede89418428004f9b3e53d0ffb7f6f6e21af0a Author: damianedwards Date: Tue May 5 23:27:25 2015 -0700 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..bdaa5ba982 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,50 @@ +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +*.jpg binary +*.png binary +*.gif binary + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..65df2761c4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*net451.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +*.sln.ide +debugSettings.json +project.lock.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..947bf868ee --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: csharp +sudo: false +script: + - ./build.sh --quiet verify \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..eac4268e4c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +Contributing +====== + +Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000..0bdc1962b6 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/Localization.sln b/Localization.sln new file mode 100644 index 0000000000..0df01140f0 --- /dev/null +++ b/Localization.sln @@ -0,0 +1,41 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.22823.1 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FB313677-BAB3-4E49-8CDB-4FA4A9564767}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Framework.Localization", "src\Microsoft.Framework.Localization\Microsoft.Framework.Localization.xproj", "{29743CFF-120E-40EB-9B89-5818425946FA}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Localization", "src\Microsoft.AspNet.Localization\Microsoft.AspNet.Localization.xproj", "{23E3BC23-3464-4D9B-BF78-02CB2182BEF0}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Framework.Localization.Abstractions", "src\Microsoft.Framework.Localization.Abstractions\Microsoft.Framework.Localization.Abstractions.xproj", "{A1FCF259-70F6-4605-AA2D-E4B356BE771A}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {29743CFF-120E-40EB-9B89-5818425946FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29743CFF-120E-40EB-9B89-5818425946FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29743CFF-120E-40EB-9B89-5818425946FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29743CFF-120E-40EB-9B89-5818425946FA}.Release|Any CPU.Build.0 = Release|Any CPU + {23E3BC23-3464-4D9B-BF78-02CB2182BEF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {23E3BC23-3464-4D9B-BF78-02CB2182BEF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {23E3BC23-3464-4D9B-BF78-02CB2182BEF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {23E3BC23-3464-4D9B-BF78-02CB2182BEF0}.Release|Any CPU.Build.0 = Release|Any CPU + {A1FCF259-70F6-4605-AA2D-E4B356BE771A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1FCF259-70F6-4605-AA2D-E4B356BE771A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1FCF259-70F6-4605-AA2D-E4B356BE771A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1FCF259-70F6-4605-AA2D-E4B356BE771A}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {29743CFF-120E-40EB-9B89-5818425946FA} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767} + {23E3BC23-3464-4D9B-BF78-02CB2182BEF0} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767} + {A1FCF259-70F6-4605-AA2D-E4B356BE771A} = {FB313677-BAB3-4E49-8CDB-4FA4A9564767} + EndGlobalSection +EndGlobal diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000000..da57d47267 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000..c0250d425f --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +Localization +========== +AppVeyor: TODO + +Travis: TODO + +Localization abstractions and implementations for ASP.NET 5 applications. + +This project is part of ASP.NET 5. You can find samples, documentation and getting started instructions for ASP.NET 5 at the [Home](https://github.com/aspnet/home) repo. diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000000..636a7618d3 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,7 @@ +init: + - git config --global core.autocrlf true +build_script: + - build.cmd --quiet verify +clone_depth: 1 +test: off +deploy: off \ No newline at end of file diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..41025afb26 --- /dev/null +++ b/build.cmd @@ -0,0 +1,28 @@ +@echo off +cd %~dp0 + +SETLOCAL +SET CACHED_NUGET=%LocalAppData%\NuGet\NuGet.exe + +IF EXIST %CACHED_NUGET% goto copynuget +echo Downloading latest version of NuGet.exe... +IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet +@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '%CACHED_NUGET%'" + +:copynuget +IF EXIST .nuget\nuget.exe goto restore +md .nuget +copy %CACHED_NUGET% .nuget\nuget.exe > nul + +:restore +IF EXIST packages\KoreBuild goto run +.nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre +.nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion + +IF "%SKIP_DNX_INSTALL%"=="1" goto run +CALL packages\KoreBuild\build\dnvm upgrade -runtime CLR -arch x86 +CALL packages\KoreBuild\build\dnvm install default -runtime CoreCLR -arch x86 + +:run +CALL packages\KoreBuild\build\dnvm use default -runtime CLR -arch x86 +packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* diff --git a/build.sh b/build.sh new file mode 100644 index 0000000000..d81164353c --- /dev/null +++ b/build.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +if test `uname` = Darwin; then + cachedir=~/Library/Caches/KBuild +else + if [ -z $XDG_DATA_HOME ]; then + cachedir=$HOME/.local/share + else + cachedir=$XDG_DATA_HOME; + fi +fi +mkdir -p $cachedir + +url=https://www.nuget.org/nuget.exe + +if test ! -f $cachedir/nuget.exe; then + wget -O $cachedir/nuget.exe $url 2>/dev/null || curl -o $cachedir/nuget.exe --location $url /dev/null +fi + +if test ! -e .nuget; then + mkdir .nuget + cp $cachedir/nuget.exe .nuget/nuget.exe +fi + +if test ! -d packages/KoreBuild; then + mono .nuget/nuget.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre + mono .nuget/nuget.exe install Sake -version 0.2 -o packages -ExcludeVersion +fi + +if ! type dnvm > /dev/null 2>&1; then + source packages/KoreBuild/build/dnvm.sh +fi + +if ! type dnx > /dev/null 2>&1; then + dnvm upgrade +fi + +mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" + diff --git a/makefile.shade b/makefile.shade new file mode 100644 index 0000000000..562494d144 --- /dev/null +++ b/makefile.shade @@ -0,0 +1,7 @@ + +var VERSION='0.1' +var FULL_VERSION='0.1' +var AUTHORS='Microsoft Open Technologies, Inc.' + +use-standard-lifecycle +k-standard-goals diff --git a/src/Microsoft.AspNet.Localization/IApplicationBuilderExtensions.cs b/src/Microsoft.AspNet.Localization/IApplicationBuilderExtensions.cs new file mode 100644 index 0000000000..96330b7bd6 --- /dev/null +++ b/src/Microsoft.AspNet.Localization/IApplicationBuilderExtensions.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 Microsoft.AspNet.Localization; + +namespace Microsoft.AspNet.Builder +{ + /// + /// Extension methods for adding the to an application. + /// + public static class IApplicationBuilderExtensions + { + /// + /// Adds the to automatically set culture information for + /// requests based on information provided by the client. + /// + /// The . + /// The . + public static IApplicationBuilder UseRequestLocalization(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Localization/IRequestCultureFeature.cs b/src/Microsoft.AspNet.Localization/IRequestCultureFeature.cs new file mode 100644 index 0000000000..65417e228d --- /dev/null +++ b/src/Microsoft.AspNet.Localization/IRequestCultureFeature.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNet.Localization +{ + /// + /// Represents the feature that provides the current request's culture information. + /// + public interface IRequestCultureFeature + { + /// + /// The of the request. + /// + RequestCulture RequestCulture { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Localization/Microsoft.AspNet.Localization.xproj b/src/Microsoft.AspNet.Localization/Microsoft.AspNet.Localization.xproj new file mode 100644 index 0000000000..d1edd7f61f --- /dev/null +++ b/src/Microsoft.AspNet.Localization/Microsoft.AspNet.Localization.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 23e3bc23-3464-4d9b-bf78-02cb2182bef0 + Microsoft.AspNet.Localization + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/Microsoft.AspNet.Localization/RequestCulture.cs b/src/Microsoft.AspNet.Localization/RequestCulture.cs new file mode 100644 index 0000000000..c0dc4a5680 --- /dev/null +++ b/src/Microsoft.AspNet.Localization/RequestCulture.cs @@ -0,0 +1,46 @@ +// 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 +{ + /// + /// 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 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) + { + 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/Microsoft.AspNet.Localization/RequestCultureFeature.cs b/src/Microsoft.AspNet.Localization/RequestCultureFeature.cs new file mode 100644 index 0000000000..730145bd30 --- /dev/null +++ b/src/Microsoft.AspNet.Localization/RequestCultureFeature.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.AspNet.Localization +{ + /// + /// Provides the current request's culture information. + /// + public class RequestCultureFeature : IRequestCultureFeature + { + /// + /// Creates a new with the specified . + /// + /// The . + public RequestCultureFeature(RequestCulture requestCulture) + { + RequestCulture = requestCulture; + } + + /// + public RequestCulture RequestCulture { get; } + } +} diff --git a/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs b/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs new file mode 100644 index 0000000000..3be800652f --- /dev/null +++ b/src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs @@ -0,0 +1,80 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; + +namespace Microsoft.AspNet.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 readonly RequestDelegate _next; + + /// + /// Creates a new . + /// + /// The representing the next middleware in the pipeline. + public RequestLocalizationMiddleware(RequestDelegate next) + { + _next = next; + } + + /// + /// Invokes the logic of the middleware. + /// + /// The . + /// A that completes when the middleware has completed processing. + public async Task Invoke(HttpContext context) + { + // TODO: Make this read from Accept-Language header, cookie, app-provided delegate, etc. + if (context.Request.QueryString.HasValue) + { + var queryCulture = context.Request.Query["culture"]; + if (!string.IsNullOrEmpty(queryCulture)) + { + var requestCulture = new RequestCulture(new CultureInfo(queryCulture)); + + context.SetFeature(new RequestCultureFeature(requestCulture)); + + var originalCulture = CultureInfo.CurrentCulture; + var originalUICulture = CultureInfo.CurrentUICulture; + + SetCurrentCulture(requestCulture); + + await _next(context); + + return; + } + } + else + { + // NOTE: The below doesn't seem to be needed anymore now that DNX is correctly managing culture across + // async calls but we'll need to verify properly. + // Forcibly set thread to en-US as sometimes previous threads have wrong culture across async calls, + // see note above. + //var defaultRequestCulture = new RequestCulture(new CultureInfo("en-US")); + //SetCurrentCulture(defaultRequestCulture); + } + + await _next(context); + } + + private void SetCurrentCulture(RequestCulture requestCulture) + { +#if DNX451 + Thread.CurrentThread.CurrentCulture = requestCulture.Culture; + Thread.CurrentThread.CurrentUICulture = requestCulture.UICulture; +#else + CultureInfo.CurrentCulture = requestCulture.Culture; + CultureInfo.CurrentUICulture = requestCulture.UICulture; +#endif + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Localization/project.json b/src/Microsoft.AspNet.Localization/project.json new file mode 100644 index 0000000000..ca6767dc74 --- /dev/null +++ b/src/Microsoft.AspNet.Localization/project.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0-*", + "description": "", + + "dependencies": { + "Microsoft.Framework.Localization.Abstractions": "1.0.0-*", + "Microsoft.AspNet.Http.Extensions": "1.0.0-*" + }, + + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "System.Collections": "4.0.10-*", + "System.Linq": "4.0.0-*", + "System.Threading": "4.0.10-*", + "Microsoft.CSharp": "4.0.0-*" + } + } + } +} diff --git a/src/Microsoft.Framework.Localization.Abstractions/IStringLocalizer.cs b/src/Microsoft.Framework.Localization.Abstractions/IStringLocalizer.cs new file mode 100644 index 0000000000..4dddf75f25 --- /dev/null +++ b/src/Microsoft.Framework.Localization.Abstractions/IStringLocalizer.cs @@ -0,0 +1,51 @@ +// 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; + +namespace Microsoft.Framework.Localization +{ + /// + /// Represents a service that provides localized strings. + /// + public interface IStringLocalizer : IEnumerable + { + /// + /// Gets the string resource with the given name. + /// + /// The name of the string resource. + /// The string resource as a . + LocalizedString this[string name] { get; } + + /// + /// Gets the string resource with the given name and formatted with the supplied arguments. + /// + /// The name of the string resource. + /// The values to format the string with. + /// The formatted string resource as a . + LocalizedString this[string name, params object[] arguments] { get; } + + /// + /// Gets the string resource with the given name. + /// + /// The name of the string resource. + /// The string resource as a . + LocalizedString GetString(string name); + + /// + /// Gets the string resource with the given name and formatted with the supplied arguments. + /// + /// The name of the string resource. + /// The values to format the string with. + /// The formatted string resource as a . + LocalizedString GetString(string name, params object[] arguments); + + /// + /// Creates a new for a specific . + /// + /// The to use. + /// A culture-specific . + IStringLocalizer WithCulture(CultureInfo culture); + } +} \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization.Abstractions/IStringLocalizerFactory.cs b/src/Microsoft.Framework.Localization.Abstractions/IStringLocalizerFactory.cs new file mode 100644 index 0000000000..0902b26277 --- /dev/null +++ b/src/Microsoft.Framework.Localization.Abstractions/IStringLocalizerFactory.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; + +namespace Microsoft.Framework.Localization +{ + /// + /// Represents a factory that creates instances. + /// + public interface IStringLocalizerFactory + { + /// + /// Creates an using the and + /// of the specified . + /// + /// The . + /// The . + IStringLocalizer Create(Type resourceSource); + + /// + /// Creates an . + /// + /// The base name of the resource to load strings from. + /// The location to load resources from. + /// The . + IStringLocalizer Create(string baseName, string location); + } +} \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization.Abstractions/IStringLocalizerOfT.cs b/src/Microsoft.Framework.Localization.Abstractions/IStringLocalizerOfT.cs new file mode 100644 index 0000000000..f81a3c0eaf --- /dev/null +++ b/src/Microsoft.Framework.Localization.Abstractions/IStringLocalizerOfT.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Framework.Localization +{ + /// + /// Represents an that provides strings for . + /// + /// The to provide strings for. + public interface IStringLocalizer : IStringLocalizer + { + + } +} \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization.Abstractions/LocalizedString.cs b/src/Microsoft.Framework.Localization.Abstractions/LocalizedString.cs new file mode 100644 index 0000000000..4faf864309 --- /dev/null +++ b/src/Microsoft.Framework.Localization.Abstractions/LocalizedString.cs @@ -0,0 +1,61 @@ +// 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.Framework.Localization +{ + /// + /// A locale specific string. + /// + public struct LocalizedString + { + /// + /// Creates a new . + /// + /// The name of the string in the resource it was loaded from. + /// The actual string. + public LocalizedString(string name, string value) + : this(name, value, resourceNotFound: false) + { + + } + + /// + /// Creates a new . + /// + /// The name of the string in the resource it was loaded from. + /// The actual string. + /// Whether the string was found in a resource. Set this to false to indicate an alternate string value was used. + public LocalizedString(string name, string value, bool resourceNotFound) + { + Name = name; + Value = value; + ResourceNotFound = resourceNotFound; + } + + public static implicit operator string (LocalizedString localizedString) + { + return localizedString.Value; + } + + /// + /// The name of the string in the resource it was loaded from. + /// + public string Name { get; } + + /// + /// The actual string. + /// + public string Value { get; } + + /// + /// Whether the string was found in a resource. If false, an alternate string value was used. + /// + public bool ResourceNotFound { get; } + + /// + /// Returns the actual string. + /// + /// The actual string. + public override string ToString() => Value; + } +} \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization.Abstractions/Microsoft.Framework.Localization.Abstractions.xproj b/src/Microsoft.Framework.Localization.Abstractions/Microsoft.Framework.Localization.Abstractions.xproj new file mode 100644 index 0000000000..7e088aa5ce --- /dev/null +++ b/src/Microsoft.Framework.Localization.Abstractions/Microsoft.Framework.Localization.Abstractions.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + a1fcf259-70f6-4605-aa2d-e4b356be771a + Microsoft.Framework.Localization.Abstractions + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/Microsoft.Framework.Localization.Abstractions/StringLocalizerOfT.cs b/src/Microsoft.Framework.Localization.Abstractions/StringLocalizerOfT.cs new file mode 100644 index 0000000000..f222bf93b3 --- /dev/null +++ b/src/Microsoft.Framework.Localization.Abstractions/StringLocalizerOfT.cs @@ -0,0 +1,48 @@ +// 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; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.Framework.Localization +{ + /// + /// Provides strings for . + /// + /// The to provide strings for. + public class StringLocalizer : IStringLocalizer + { + private IStringLocalizer _localizer; + + /// + /// Creates a new . + /// + /// The to use. + public StringLocalizer(IStringLocalizerFactory factory) + { + _localizer = factory.Create(typeof(TResourceSource)); + } + + /// + public virtual IStringLocalizer WithCulture(CultureInfo culture) => _localizer.WithCulture(culture); + + /// + public virtual LocalizedString this[string key] => _localizer[key]; + + /// + public virtual LocalizedString this[string key, params object[] arguments] => _localizer[key, arguments]; + + /// + public virtual LocalizedString GetString(string key) => _localizer.GetString(key); + + /// + public virtual LocalizedString GetString(string key, params object[] arguments) => + _localizer.GetString(key, arguments); + + /// + public IEnumerator GetEnumerator() => _localizer.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => _localizer.GetEnumerator(); + } +} \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization.Abstractions/project.json b/src/Microsoft.Framework.Localization.Abstractions/project.json new file mode 100644 index 0000000000..87aa883bfd --- /dev/null +++ b/src/Microsoft.Framework.Localization.Abstractions/project.json @@ -0,0 +1,21 @@ +{ + "version": "1.0.0-*", + "description": "Abstractions of application localization services.", + + "dependencies": { + + }, + + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "System.Collections": "4.0.10-beta-*", + "System.Globalization": "4.0.10-beta-*", + "System.Reflection": "4.0.10-beta-*", + "System.Runtime": "4.0.20-beta-22904", + "Microsoft.CSharp": "4.0.0-beta-*" + } + } + } +} diff --git a/src/Microsoft.Framework.Localization/IServiceCollectionExtensions.cs b/src/Microsoft.Framework.Localization/IServiceCollectionExtensions.cs new file mode 100644 index 0000000000..9b4a0079fc --- /dev/null +++ b/src/Microsoft.Framework.Localization/IServiceCollectionExtensions.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.Framework.Localization; + +namespace Microsoft.Framework.DependencyInjection +{ + /// + /// Extension methods for adding localization servics to the DI container. + /// + public static class LocalizationServiceCollectionExtensions + { + /// + /// Adds services required for application localization. + /// + /// The to add the services to. + /// The . + public static IServiceCollection AddLocalization(this IServiceCollection services) + { + services.TryAdd(new ServiceDescriptor( + typeof(IStringLocalizerFactory), + typeof(ResourceManagerStringLocalizerFactory), + ServiceLifetime.Singleton)); + services.TryAdd(new ServiceDescriptor( + typeof(IStringLocalizer<>), + typeof(StringLocalizer<>), + ServiceLifetime.Transient)); + + return services; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization/Microsoft.Framework.Localization.xproj b/src/Microsoft.Framework.Localization/Microsoft.Framework.Localization.xproj new file mode 100644 index 0000000000..487327e0ee --- /dev/null +++ b/src/Microsoft.Framework.Localization/Microsoft.Framework.Localization.xproj @@ -0,0 +1,20 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 29743cff-120e-40eb-9b89-5818425946fa + Microsoft.Framework.Localization + ..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + diff --git a/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizer.cs b/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizer.cs new file mode 100644 index 0000000000..b6db0b6d13 --- /dev/null +++ b/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizer.cs @@ -0,0 +1,226 @@ +// 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; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Globalization; +using System.Reflection; +using System.Resources; + +namespace Microsoft.Framework.Localization +{ + /// + /// An that uses the and + /// to provide localized strings. + /// + public class ResourceManagerStringLocalizer : IStringLocalizer + { + private readonly ConcurrentDictionary _missingManifestCache = + new ConcurrentDictionary(); + + /// + /// Creates a new . + /// + /// The to read strings from. + /// The that contains the strings as embedded resources. + /// The base name of the embedded resource in the that contains the strings. + public ResourceManagerStringLocalizer( + ResourceManager resourceManager, + Assembly resourceAssembly, + string baseName) + { + ResourceManager = resourceManager; + ResourceAssembly = resourceAssembly; + ResourceBaseName = baseName; + } + + /// + /// The to read strings from. + /// + protected ResourceManager ResourceManager { get; } + + /// + /// The that contains the strings as embedded resources. + /// + protected Assembly ResourceAssembly { get; } + + /// + /// The base name of the embedded resource in the that contains the strings. + /// + protected string ResourceBaseName { get; } + + /// + public virtual LocalizedString this[string name] => GetString(name); + + /// + public virtual LocalizedString this[string name, params object[] arguments] => GetString(name, arguments); + + /// + public virtual LocalizedString GetString(string name) + { + var value = GetStringSafely(name, null); + return new LocalizedString(name, value ?? name, resourceNotFound: value == null); + } + + /// + public virtual LocalizedString GetString(string name, params object[] arguments) + { + var format = GetStringSafely(name, null); + var value = string.Format(format ?? name, arguments); + return new LocalizedString(name, value, resourceNotFound: format == null); + } + + /// + /// Creates a new for a specific . + /// + /// The to use. + /// A culture-specific . + public IStringLocalizer WithCulture(CultureInfo culture) + { + return culture == null + ? new ResourceManagerStringLocalizer(ResourceManager, ResourceAssembly, ResourceBaseName) + : new ResourceManagerWithCultureStringLocalizer( + ResourceManager, + ResourceAssembly, + ResourceBaseName, + culture); + } + + /// + /// Gets a resource string from the and returns null instead of + /// throwing exceptions if a match isn't found. + /// + /// The name of the string resource. + /// The to get the string for. + /// The resource string, or null if none was found. + protected string GetStringSafely(string name, CultureInfo culture) + { + var cacheKey = new MissingManifestCacheKey(name, culture); + if (_missingManifestCache.ContainsKey(cacheKey)) + { + return null; + } + + try + { + return culture == null ? ResourceManager.GetString(name) : ResourceManager.GetString(name, culture); + } + catch (MissingManifestResourceException) + { + _missingManifestCache.TryAdd(cacheKey, null); + return null; + } + } + + /// + /// Returns an for all strings in the current culture. + /// + /// The . + public virtual IEnumerator GetEnumerator() + { + return GetEnumerator(CultureInfo.CurrentUICulture); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Returns an for all strings in the specified culture. + /// + /// The to get strings for. + /// The . + protected IEnumerator GetEnumerator(CultureInfo culture) + { + // TODO: I'm sure something here should be cached, probably the whole result + var resourceNames = GetResourceNamesFromCultureHierarchy(culture); + + foreach (var name in resourceNames) + { + var value = GetStringSafely(name, culture); + yield return new LocalizedString(name, value ?? name, resourceNotFound: value == null); + } + } + + private IEnumerable GetResourceNamesFromCultureHierarchy(CultureInfo startingCulture) + { + var currentCulture = startingCulture; + var resourceNames = new HashSet(); + + while (true) + { + try + { + var resourceStreamName = ResourceBaseName; + if (!string.IsNullOrEmpty(currentCulture.Name)) + { + resourceStreamName += "." + currentCulture.Name; + } + resourceStreamName += ".resources"; + using (var cultureResourceStream = ResourceAssembly.GetManifestResourceStream(resourceStreamName)) + using (var resources = new ResourceReader(cultureResourceStream)) + { + foreach (DictionaryEntry entry in resources) + { + var resourceName = (string)entry.Key; + resourceNames.Add(resourceName); + } + } + } + catch (MissingManifestResourceException) { } + + if (currentCulture == currentCulture.Parent) + { + // currentCulture begat currentCulture, probably time to leave + break; + } + + currentCulture = currentCulture.Parent; + } + + return resourceNames; + } + + private class MissingManifestCacheKey : IEquatable + { + private readonly int _hashCode; + + public MissingManifestCacheKey(string name, CultureInfo culture) + { + Name = name; + CultureInfo = culture; + _hashCode = new { Name, CultureInfo }.GetHashCode(); + } + + public string Name { get; } + + public CultureInfo CultureInfo { get; } + + public bool Equals(MissingManifestCacheKey other) + { + return string.Equals(Name, other.Name, StringComparison.Ordinal) + && CultureInfo == other.CultureInfo; + } + + public override bool Equals(object obj) + { + var other = obj as MissingManifestCacheKey; + + if (other != null) + { + return Equals(other); + } + + return base.Equals(obj); + } + + public override int GetHashCode() + { + return _hashCode; + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizerFactory.cs b/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizerFactory.cs new file mode 100644 index 0000000000..801f6acef5 --- /dev/null +++ b/src/Microsoft.Framework.Localization/ResourceManagerStringLocalizerFactory.cs @@ -0,0 +1,54 @@ +// 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.Reflection; +using System.Resources; +using Microsoft.Framework.Runtime; + +namespace Microsoft.Framework.Localization +{ + /// + /// An that creates instances of . + /// + public class ResourceManagerStringLocalizerFactory : IStringLocalizerFactory + { + private readonly IApplicationEnvironment _applicationEnvironment; + + /// + /// Creates a new . + /// + /// + public ResourceManagerStringLocalizerFactory(IApplicationEnvironment applicationEnvironment) + { + _applicationEnvironment = applicationEnvironment; + } + + /// + /// Creates a using the and + /// of the specified . + /// + /// The . + /// The . + public IStringLocalizer Create(Type resourceSource) + { + var typeInfo = resourceSource.GetTypeInfo(); + var assembly = typeInfo.Assembly; + var baseName = typeInfo.FullName; + return new ResourceManagerStringLocalizer(new ResourceManager(resourceSource), assembly, baseName); + } + + /// + /// Creates a . + /// + /// The base name of the resource to load strings from. + /// The location to load resources from. + /// The . + public IStringLocalizer Create(string baseName, string location) + { + var assembly = Assembly.Load(new AssemblyName(location ?? _applicationEnvironment.ApplicationName)); + + return new ResourceManagerStringLocalizer(new ResourceManager(baseName, assembly), assembly, baseName); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization/ResourceManagerWithCultureStringLocalizer.cs b/src/Microsoft.Framework.Localization/ResourceManagerWithCultureStringLocalizer.cs new file mode 100644 index 0000000000..7b988b04ba --- /dev/null +++ b/src/Microsoft.Framework.Localization/ResourceManagerWithCultureStringLocalizer.cs @@ -0,0 +1,64 @@ +// 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.Reflection; +using System.Resources; + +namespace Microsoft.Framework.Localization +{ + /// + /// An that uses the and + /// to provide localized strings for a specific . + /// + public class ResourceManagerWithCultureStringLocalizer : ResourceManagerStringLocalizer + { + private readonly CultureInfo _culture; + + /// + /// Creates a new . + /// + /// The to read strings from. + /// The that contains the strings as embedded resources. + /// The base name of the embedded resource in the that contains the strings. + /// The specific to use. + public ResourceManagerWithCultureStringLocalizer( + ResourceManager resourceManager, + Assembly assembly, + string baseName, + CultureInfo culture) + : base(resourceManager, assembly, baseName) + { + _culture = culture; + } + + /// + public override LocalizedString this[string name] => GetString(name); + + /// + public override LocalizedString this[string name, params object[] arguments] => GetString(name, arguments); + + /// + public override LocalizedString GetString(string name) + { + var value = GetStringSafely(name, _culture); + return new LocalizedString(name, value ?? name); + } + + /// + public override LocalizedString GetString(string name, params object[] arguments) + { + var format = GetStringSafely(name, _culture); + var value = string.Format(_culture, format ?? name, arguments); + return new LocalizedString(name, value ?? name, resourceNotFound: format == null); + } + + /// + public override IEnumerator GetEnumerator() + { + return GetEnumerator(_culture); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Framework.Localization/project.json b/src/Microsoft.Framework.Localization/project.json new file mode 100644 index 0000000000..32c9bd7b33 --- /dev/null +++ b/src/Microsoft.Framework.Localization/project.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0-*", + "description": "", + + "dependencies": { + "Microsoft.Framework.DependencyInjection.Abstractions": "1.0.0-*", + "Microsoft.Framework.Localization.Abstractions": "1.0.0-*", + "Microsoft.Framework.Runtime.Abstractions": "1.0.0-*", + "System.Resources.ReaderWriter": "4.0.0-*" + }, + + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "System.Collections": "4.0.10-*", + "System.Collections.Concurrent": "4.0.10-*", + "System.Globalization": "4.0.10-*", + "System.Linq": "4.0.0-*", + "System.Resources.ResourceManager": "4.0.0-*", + "System.Threading": "4.0.10-*", + "Microsoft.CSharp": "4.0.0-*" + } + } + } +}