Improve resource file support (#2511)

* Add satellite assemblies to deps file with locale data
* Publish satellite assemblies to output during publish
* Copy satellite assemblies from project-to-project dependencies on
build and publish
This commit is contained in:
Andrew Stanton-Nurse 2016-04-15 14:45:51 -07:00 committed by David Fowler
parent 777e75f0a9
commit 852446e859
22 changed files with 519 additions and 15 deletions

View file

@ -0,0 +1,12 @@
using System;
namespace ConsoleApplication
{
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}

View file

@ -0,0 +1,19 @@
{
"version": "1.0.0-*",
"compilationOptions": {
"emitEntryPoint": true
},
"dependencies": {
"Microsoft.Data.OData": "5.6.4",
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.0-rc2-24008"
},
"TestLibraryWithResources": { "target": "project" }
},
"frameworks": {
"netcoreapp1.0": {
"imports": [ "portable-net45+win8" ]
}
}
}

View file

@ -0,0 +1,20 @@
using System;
using System.Resources;
using System.Reflection;
using System.Globalization;
namespace TestProjectWithCultureSpecificResource
{
public class Program
{
public static void Main(string[] args)
{
var rm = new ResourceManager(
"TestProjectWithCultureSpecificResource.Strings",
typeof(Program).GetTypeInfo().Assembly);
Console.WriteLine(rm.GetString("hello"));
Console.WriteLine(rm.GetString("hello", new CultureInfo("fr")));
}
}
}

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="hello" xml:space="preserve">
<value>Bonjour!</value>
</data>
</root>

View file

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="hello" xml:space="preserve">
<value>Hello World!</value>
</data>
</root>

View file

@ -0,0 +1,11 @@
{
"version": "1.0.0-*",
"dependencies": {
"NETStandard.Library": "1.5.0-rc2-24008",
},
"frameworks": {
"netstandard1.5": {
"imports": "dnxcore50"
}
}
}

View file

@ -4,7 +4,7 @@
"emitEntryPoint": true
},
"dependencies": {
"NETStandard.Library": "1.5.0-rc2-24015"
"NETStandard.Library": "1.5.0-rc2-24008"
},
"frameworks": {
"netstandardapp1.5": {

View file

@ -358,12 +358,9 @@ namespace Microsoft.DotNet.Cli.Build
Utils.DeleteDirectory(SharedFrameworkNameAndVersionRoot);
}
string publishFramework = "dnxcore50"; // Temporary, use "netcoreapp" when we update nuget.
dotnetCli.Publish(
"--output", SharedFrameworkNameAndVersionRoot,
"-r", sharedFrameworkRid,
"-f", publishFramework,
SharedFrameworkSourceRoot).Execute().EnsureSuccessful();
// Clean up artifacts that dotnet-publish generates which we don't need

View file

@ -97,6 +97,16 @@ namespace Microsoft.Dotnet.Cli.Compiler.Common
{
libraryExport.RuntimeAssemblyGroups.GetDefaultAssets().CopyTo(_runtimeOutputPath);
libraryExport.NativeLibraryGroups.GetDefaultAssets().CopyTo(_runtimeOutputPath);
foreach(var group in libraryExport.ResourceAssemblies.GroupBy(r => r.Locale))
{
var localeSpecificDir = Path.Combine(_runtimeOutputPath, group.Key);
if(!Directory.Exists(localeSpecificDir))
{
Directory.CreateDirectory(localeSpecificDir);
}
group.Select(r => r.Asset).CopyTo(localeSpecificDir);
}
}
}

View file

@ -196,6 +196,7 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
var builder = LibraryExportBuilder.Create(library);
builder.AddNativeLibraryGroup(new LibraryAssetGroup(PopulateAssets(library, library.NativeLibraries)));
builder.AddRuntimeAssemblyGroup(new LibraryAssetGroup(PopulateAssets(library, library.RuntimeAssemblies)));
builder.WithResourceAssemblies(PopulateResources(library, library.ResourceAssemblies));
builder.WithCompilationAssemblies(PopulateAssets(library, library.CompileTimeAssemblies));
if (library.Identity.Type.Equals(LibraryType.Package))
@ -322,6 +323,10 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
builder.AddRuntimeAssemblyGroup(new LibraryAssetGroup(new[] { compilationAssemblyAsset }));
builder.WithRuntimeAssets(CollectAssets(outputPaths.CompilationFiles));
}
builder.WithResourceAssemblies(outputPaths.CompilationFiles.Resources().Select(r => new LibraryResourceAssembly(
LibraryAsset.CreateFromAbsolutePath(outputPaths.CompilationFiles.BasePath, r.Path),
r.Locale)));
}
builder.WithSourceReferences(project.Project.Files.SharedFiles.Select(f =>
@ -334,7 +339,7 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
private IEnumerable<LibraryAsset> CollectAssets(CompilationOutputFiles files)
{
var assemblyPath = files.Assembly;
foreach (var path in files.All())
foreach (var path in files.All().Except(files.Resources().Select(r => r.Path)))
{
if (string.Equals(assemblyPath, path))
{
@ -458,6 +463,21 @@ namespace Microsoft.DotNet.ProjectModel.Compilation
return analyzerRefs;
}
private IEnumerable<LibraryResourceAssembly> PopulateResources(TargetLibraryWithAssets library, IEnumerable<LockFileItem> section)
{
foreach (var assemblyPath in section.Where(a => !PackageDependencyProvider.IsPlaceholderFile(a.Path)))
{
string locale;
if(!assemblyPath.Properties.TryGetValue(Constants.LocaleLockFilePropertyName, out locale))
{
locale = null;
}
yield return new LibraryResourceAssembly(
LibraryAsset.CreateFromRelativePath(library.Path, assemblyPath.Path),
locale);
}
}
private IEnumerable<LibraryAsset> PopulateAssets(TargetLibraryWithAssets library, IEnumerable<LockFileItem> section)
{
foreach (var assemblyPath in section.Where(a => !PackageDependencyProvider.IsPlaceholderFile(a.Path)))

View file

@ -56,7 +56,7 @@ namespace Microsoft.DotNet.ProjectModel
public string OutputExtension { get; }
public virtual IEnumerable<string> Resources()
public virtual IEnumerable<ResourceFile> Resources()
{
var resourceNames = Project.Files.ResourceFiles
.Select(f => ResourceUtility.GetResourceCultureName(f.Key))
@ -65,7 +65,7 @@ namespace Microsoft.DotNet.ProjectModel
foreach (var resourceName in resourceNames)
{
yield return Path.Combine(BasePath, resourceName, Project.Name + ".resources" + FileNameSuffixes.DotNet.DynamicLib);
yield return new ResourceFile(Path.Combine(BasePath, resourceName, Project.Name + ".resources" + FileNameSuffixes.DotNet.DynamicLib), resourceName);
}
}
@ -80,7 +80,7 @@ namespace Microsoft.DotNet.ProjectModel
}
foreach (var resource in Resources())
{
yield return resource;
yield return resource.Path;
}
}
}

View file

@ -10,6 +10,8 @@ namespace Microsoft.DotNet.ProjectModel
public static readonly string DefaultOutputDirectory = "bin";
public static readonly string DefaultConfiguration = "Debug";
public static readonly string LocaleLockFilePropertyName = "locale";
public static readonly Version Version50 = new Version(5, 0);
}
}

View file

@ -7,9 +7,24 @@ namespace Microsoft.DotNet.ProjectModel.Graph
{
public class LockFileItem
{
public LockFileItem()
{
Properties = new Dictionary<string, string>();;
}
public LockFileItem(string path) : this()
{
Path = path;
}
public LockFileItem(string path, IDictionary<string, string> properties) : this(path)
{
Properties = new Dictionary<string, string>(properties);
}
public string Path { get; set; }
public IDictionary<string, string> Properties { get; } = new Dictionary<string, string>();
public IDictionary<string, string> Properties { get; }
public static implicit operator string (LockFileItem item) => item.Path;

View file

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Microsoft.DotNet.ProjectModel
{
public class ResourceFile
{
public string Path { get; }
public string Locale { get; }
public ResourceFile(string path, string locale)
{
Path = path;
Locale = locale;
}
}
}

View file

@ -147,6 +147,16 @@ namespace Microsoft.DotNet.Tools.Publish
var runtimeAssetsToCopy = export.RuntimeAssets.Where(a => ShouldCopyExportRuntimeAsset(context, buildOutputPaths, export, a));
runtimeAssetsToCopy.StructuredCopyTo(outputPath, outputPaths.IntermediateOutputDirectoryPath);
foreach(var resourceAsset in export.ResourceAssemblies)
{
var dir = Path.Combine(outputPath, resourceAsset.Locale);
if(!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
File.Copy(resourceAsset.Asset.ResolvedPath, Path.Combine(dir, resourceAsset.Asset.FileName), overwrite: true);
}
}
if (context.ProjectFile.HasRuntimeOutput(configuration) && !context.TargetFramework.IsDesktop())

View file

@ -10,7 +10,7 @@
"$(RID)": {}
},
"frameworks": {
"dnxcore50": {
"netcoreapp1.0": {
"imports": [
"portable-net45+win8"
]

View file

@ -126,6 +126,34 @@ namespace Microsoft.DotNet.ProjectModel.Tests
runtimeAsset.ResolvedPath.Should().Be(Path.Combine(PackagePath, "lib/Something.OSX.dll"));
}
[Fact]
public void ExportsPackageResourceAssemblies()
{
var description = CreateDescription(
new LockFileTargetLibrary()
{
ResourceAssemblies = new List<LockFileItem>()
{
new LockFileItem("resources/en-US/Res.dll", new Dictionary<string, string>() { { "locale", "en-US"} }),
new LockFileItem("resources/ru-RU/Res.dll", new Dictionary<string, string>() { { "locale", "ru-RU" } }),
}
});
var result = ExportSingle(description);
result.ResourceAssemblies.Should().HaveCount(2);
var asset = result.ResourceAssemblies.Should().Contain(g => g.Locale == "en-US").Subject.Asset;
asset.Name.Should().Be("Res");
asset.Transform.Should().BeNull();
asset.RelativePath.Should().Be("resources/en-US/Res.dll");
asset.ResolvedPath.Should().Be(Path.Combine(PackagePath, "resources/en-US/Res.dll"));
asset = result.ResourceAssemblies.Should().Contain(g => g.Locale == "ru-RU").Subject.Asset;
asset.Name.Should().Be("Res");
asset.Transform.Should().BeNull();
asset.RelativePath.Should().Be("resources/ru-RU/Res.dll");
asset.ResolvedPath.Should().Be(Path.Combine(PackagePath, "resources/ru-RU/Res.dll"));
}
[Fact]
public void ExportsSources()
{

View file

@ -252,6 +252,24 @@ namespace Microsoft.Extensions.DependencyModel.Tests
asm.Assemblies.Should().OnlyContain(a => a == "System.Collections.dll");
}
[Fact]
public void FillsResources()
{
var context = Build(runtimeExports: new[]
{
Export(PackageDescription("Pack.Age", version: new NuGetVersion(1, 2, 3)),
resourceAssemblies: new []
{
new LibraryResourceAssembly(new LibraryAsset("Dll", "resources/en-US/Pack.Age.dll", ""), "en-US")
})
});
context.RuntimeLibraries.Should().HaveCount(1);
var lib = context.RuntimeLibraries.Should().Contain(l => l.Name == "Pack.Age").Subject;
lib.ResourceAssemblies.Should().OnlyContain(l => l.Locale == "en-US" && l.Path == "resources/en-US/Pack.Age.dll");
}
[Fact]
public void ReferenceAssembliesPathRelativeToDefaultRoot()
{

View file

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@ -8,6 +9,7 @@ using FluentAssertions;
using Microsoft.DotNet.ProjectModel;
using Microsoft.DotNet.Tools.Test.Utilities;
using Microsoft.Extensions.PlatformAbstractions;
using Newtonsoft.Json.Linq;
using NuGet.Frameworks;
using Xunit;
@ -212,6 +214,41 @@ namespace Microsoft.DotNet.Tools.Builder.Tests
}
}
[Fact]
public void PackageReferenceWithResourcesTest()
{
var testInstance = TestAssetsManager.CreateTestInstance("ResourcesTests")
.WithLockFiles();
var projectRoot = Path.Combine(testInstance.TestRoot, "TestApp");
var cmd = new BuildCommand(projectRoot);
var result = cmd.Execute();
result.Should().Pass();
var outputDir = new DirectoryInfo(Path.Combine(projectRoot, "bin", "Debug", "netcoreapp1.0"));
outputDir.Should().HaveFile("TestLibraryWithResources.dll");
outputDir.Sub("fr").Should().HaveFile("TestLibraryWithResources.resources.dll");
var depsJson = JObject.Parse(File.ReadAllText(Path.Combine(outputDir.FullName, $"{Path.GetFileNameWithoutExtension(cmd.GetOutputExecutableName())}.deps.json")));
foreach (var library in new[] { Tuple.Create("Microsoft.Data.OData", "5.6.4"), Tuple.Create("TestLibraryWithResources", "1.0.0") })
{
var resources = depsJson["targets"][".NETCoreApp,Version=v1.0"][library.Item1 + "/" + library.Item2]["resources"];
resources.Should().NotBeNull();
foreach (var item in resources.Children<JProperty>())
{
var locale = item.Value["locale"];
locale.Should().NotBeNull();
item.Name.Should().EndWith($"{locale}/{library.Item1}.resources.dll");
}
}
}
[Fact]
public void ResourceTest()
{

View file

@ -42,5 +42,45 @@ namespace Microsoft.DotNet.Tools.Publish.Tests
.Should()
.HaveFile("config.xml");
}
[Fact]
public void PublishTestAppWithReferencesToResources()
{
var testInstance = TestAssetsManager.CreateTestInstance("ResourcesTests")
.WithLockFiles();
var projectRoot = Path.Combine(testInstance.TestRoot, "TestApp");
var publishCommand = new PublishCommand(projectRoot);
var publishResult = publishCommand.Execute();
publishResult.Should().Pass();
var publishDir = publishCommand.GetOutputDirectory(portable: true);
publishDir.Should().HaveFiles(new[]
{
"TestApp.dll",
"TestApp.deps.json"
});
foreach (var culture in new[] { "de", "es", "fr", "it", "ja", "ko", "ru", "zh-Hans", "zh-Hant" })
{
var cultureDir = publishDir.Sub(culture);
// Provided by packages
cultureDir.Should().HaveFiles(new[] {
"Microsoft.Data.Edm.resources.dll",
"Microsoft.Data.OData.resources.dll",
"System.Spatial.resources.dll"
});
// Check for the project-to-project one
if (culture == "fr")
{
cultureDir.Should().HaveFile("TestLibraryWithResources.resources.dll");
}
}
}
}
}