From 9eede89418428004f9b3e53d0ffb7f6f6e21af0a Mon Sep 17 00:00:00 2001 From: damianedwards Date: Tue, 5 May 2015 23:27:25 -0700 Subject: [PATCH] Initial commit --- .gitattributes | 50 ++++ .gitignore | 28 +++ .travis.yml | 4 + CONTRIBUTING.md | 4 + LICENSE.txt | 12 + Localization.sln | 41 ++++ NuGet.Config | 7 + README.md | 9 + appveyor.yml | 7 + build.cmd | 28 +++ build.sh | 39 +++ makefile.shade | 7 + .../IApplicationBuilderExtensions.cs | 24 ++ .../IRequestCultureFeature.cs | 16 ++ .../Microsoft.AspNet.Localization.xproj | 20 ++ .../RequestCulture.cs | 46 ++++ .../RequestCultureFeature.cs | 23 ++ .../RequestLocalizationMiddleware.cs | 80 +++++++ .../project.json | 21 ++ .../IStringLocalizer.cs | 51 ++++ .../IStringLocalizerFactory.cs | 29 +++ .../IStringLocalizerOfT.cs | 16 ++ .../LocalizedString.cs | 61 +++++ ....Framework.Localization.Abstractions.xproj | 20 ++ .../StringLocalizerOfT.cs | 48 ++++ .../project.json | 21 ++ .../IServiceCollectionExtensions.cs | 32 +++ .../Microsoft.Framework.Localization.xproj | 20 ++ .../ResourceManagerStringLocalizer.cs | 226 ++++++++++++++++++ .../ResourceManagerStringLocalizerFactory.cs | 54 +++++ ...sourceManagerWithCultureStringLocalizer.cs | 64 +++++ .../project.json | 26 ++ 32 files changed, 1134 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 Localization.sln create mode 100644 NuGet.Config create mode 100644 README.md create mode 100644 appveyor.yml create mode 100644 build.cmd create mode 100644 build.sh create mode 100644 makefile.shade create mode 100644 src/Microsoft.AspNet.Localization/IApplicationBuilderExtensions.cs create mode 100644 src/Microsoft.AspNet.Localization/IRequestCultureFeature.cs create mode 100644 src/Microsoft.AspNet.Localization/Microsoft.AspNet.Localization.xproj create mode 100644 src/Microsoft.AspNet.Localization/RequestCulture.cs create mode 100644 src/Microsoft.AspNet.Localization/RequestCultureFeature.cs create mode 100644 src/Microsoft.AspNet.Localization/RequestLocalizationMiddleware.cs create mode 100644 src/Microsoft.AspNet.Localization/project.json create mode 100644 src/Microsoft.Framework.Localization.Abstractions/IStringLocalizer.cs create mode 100644 src/Microsoft.Framework.Localization.Abstractions/IStringLocalizerFactory.cs create mode 100644 src/Microsoft.Framework.Localization.Abstractions/IStringLocalizerOfT.cs create mode 100644 src/Microsoft.Framework.Localization.Abstractions/LocalizedString.cs create mode 100644 src/Microsoft.Framework.Localization.Abstractions/Microsoft.Framework.Localization.Abstractions.xproj create mode 100644 src/Microsoft.Framework.Localization.Abstractions/StringLocalizerOfT.cs create mode 100644 src/Microsoft.Framework.Localization.Abstractions/project.json create mode 100644 src/Microsoft.Framework.Localization/IServiceCollectionExtensions.cs create mode 100644 src/Microsoft.Framework.Localization/Microsoft.Framework.Localization.xproj create mode 100644 src/Microsoft.Framework.Localization/ResourceManagerStringLocalizer.cs create mode 100644 src/Microsoft.Framework.Localization/ResourceManagerStringLocalizerFactory.cs create mode 100644 src/Microsoft.Framework.Localization/ResourceManagerWithCultureStringLocalizer.cs create mode 100644 src/Microsoft.Framework.Localization/project.json 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-*" + } + } + } +}