diff --git a/README-V2.md b/README-V2.md index ccec8fcc..5127a44c 100644 --- a/README-V2.md +++ b/README-V2.md @@ -1161,7 +1161,7 @@ templater.ApplyTemplate(path, templatePath, value, config) ### Attributes and configuration -#### 1. Specify the column name, column index, or ignore the column entirely +#### 1. Specify the column name, column index, or ignore the column entirely. ![image](https://user-images.githubusercontent.com/12729184/114230869-3e163700-99ac-11eb-9a90-2039d4b4b313.png) @@ -1300,7 +1300,22 @@ public class Dto } ``` -#### 8. DynamicColumnAttribute +#### 8. Resource based localization + +Support for localizable resources is available for both `MiniExcelColumnAttribute` and `MiniExcelColumnNameAttribute`: + +```csharp +public class Dto +{ + [MiniExcelColumn(Name = "Column1", ResourceType = typeof(MyResources))] + public string Test1 { get; set; } + + [MiniExcelColumnName("Column2", ResourceType = typeof(MyResources))] + public string Test2 { get; set; } +} +``` + +#### 9. DynamicColumnAttribute Attributes can also be set on columns dynamically: ```csharp @@ -1321,7 +1336,7 @@ var exporter = MiniExcel.Exporters.GetOpenXmlExporter(); exporter.Export(path, value, configuration: config); ``` -#### 9. MiniExcelSheetAttribute +#### 10. MiniExcelSheetAttribute It is possible to define the name and visibility of a sheet through the `MiniExcelSheetAttribute`: diff --git a/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs b/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs index a49e964f..1c139327 100644 --- a/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs +++ b/src/MiniExcel.Core/Attributes/MiniExcelColumnAttribute.cs @@ -1,9 +1,10 @@ +using System.Resources; + namespace MiniExcelLib.Core.Attributes; public class MiniExcelColumnAttribute : MiniExcelAttributeBase { - private int _index = -1; - private string? _xName; + private ResourceManager? _resourceManager; public string? Name { get; set; } public string[]? Aliases { get; set; } = []; @@ -15,39 +16,73 @@ public class MiniExcelColumnAttribute : MiniExcelAttributeBase public double Width { get; set; } = 8.42857143; public ColumnType Type { get; set; } = ColumnType.Value; + private int _index = -1; public int Index { get => _index; set => Init(value); } + private string? _indexName; public string? IndexName { - get => _xName; + get => _indexName; set => Init(CellReferenceConverter.GetNumericalIndex(value), value); } + private Type? _resourceType; + public Type? ResourceType + { + get => _resourceType; + set + { + if (_resourceType == value) + return; + + _resourceType = value; + if (value is null) + return; + + const BindingFlags bindingFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + if (value.GetProperty(nameof(ResourceManager), bindingFlags) is { } property && + property.GetValue(null) is ResourceManager resourceManager) + { + _resourceManager = resourceManager; + } + else + { + _resourceManager = new ResourceManager(value); + } + } + } + + internal string? GetColumnName(string? resourceKey = null) + { + if (Name is not null) + return _resourceManager?.GetString(Name) ?? Name; + + if (resourceKey is not null) + return _resourceManager?.GetString(resourceKey) ?? resourceKey; + + return null; + } + private void Init(int index, string? columnName = null) { if (index < 0) throw new ArgumentOutOfRangeException(nameof(index), index, $"Column index {index} must be greater or equal to zero."); _index = index; - _xName ??= columnName ?? CellReferenceConverter.GetAlphabeticalIndex(index); + _indexName ??= columnName ?? CellReferenceConverter.GetAlphabeticalIndex(index); } public void SetFormatId(int formatId) => FormatId = formatId; } -public class DynamicExcelColumn : MiniExcelColumnAttribute +public class DynamicExcelColumn(string key) : MiniExcelColumnAttribute { - public string Key { get; set; } + public string Key { get; set; } = key; public Func? CustomFormatter { get; set; } - - public DynamicExcelColumn(string key) - { - Key = key; - } } public enum ColumnType { Value, Formula } diff --git a/src/MiniExcel.Core/Attributes/MiniExcelColumnNameAttribute.cs b/src/MiniExcel.Core/Attributes/MiniExcelColumnNameAttribute.cs index a323220b..cf2c74ba 100644 --- a/src/MiniExcel.Core/Attributes/MiniExcelColumnNameAttribute.cs +++ b/src/MiniExcel.Core/Attributes/MiniExcelColumnNameAttribute.cs @@ -1,7 +1,43 @@ +using System.Resources; + namespace MiniExcelLib.Core.Attributes; public class MiniExcelColumnNameAttribute(string columnName, string[]? aliases = null) : MiniExcelAttributeBase { + private ResourceManager? _resourceManager; + + [Obsolete("Please use the \"Name\" property instead")] public string ExcelColumnName { get; set; } = columnName; + public string Name { get; set; } = columnName; public string[] Aliases { get; set; } = aliases ?? []; -} \ No newline at end of file + + private Type? _resourceType; + public Type? ResourceType + { + get => _resourceType; + set + { + if (_resourceType == value) + return; + + _resourceType = value; + if (value is null) + return; + + const BindingFlags bindingFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; + if (value.GetProperty(nameof(ResourceManager), bindingFlags) is { } property && + property.GetValue(null) is ResourceManager resourceManager) + { + _resourceManager = resourceManager; + } + else + { + _resourceManager = new ResourceManager(value); + } + } + } + + internal string? GetColumnName() => !string.IsNullOrEmpty(Name) + ? _resourceManager?.GetString(Name) ?? Name + : null; +} diff --git a/src/MiniExcel.Core/Reflection/ColumnMappingsProvider.cs b/src/MiniExcel.Core/Reflection/ColumnMappingsProvider.cs index 5a9f1e72..e3d4ad4d 100644 --- a/src/MiniExcel.Core/Reflection/ColumnMappingsProvider.cs +++ b/src/MiniExcel.Core/Reflection/ColumnMappingsProvider.cs @@ -139,7 +139,12 @@ internal static List GetMappingsForImport(Type type, str ExcludeNullableType = accessor.Type, Nullable = accessor.IsNullable, ExcelColumnAliases = excelColumnName?.Aliases ?? excelColumn?.Aliases ?? [], - ExcelColumnName = excelColumnName?.ExcelColumnName ?? m.GetAttribute()?.DisplayName ?? excelColumn?.Name ?? m.Name, + + ExcelColumnName = excelColumnName?.GetColumnName() + ?? excelColumn?.GetColumnName(m.Name) + ?? m.GetAttribute()?.DisplayName + ?? m.Name, + ExcelColumnIndex = m.GetAttribute()?.ExcelColumnIndex ?? excelColumnIndex, ExcelIndexName = m.GetAttribute()?.ExcelXName ?? excelColumn?.IndexName, ExcelColumnWidth = m.GetAttribute()?.ExcelColumnWidth ?? excelColumn?.Width, @@ -199,7 +204,7 @@ private static void SetDictionaryColumnInfo(List mappin if (dynamicColumn.IndexName is { } idxName) map.ExcelIndexName = idxName; - if (dynamicColumn.Name is { } colName) + if (dynamicColumn.GetColumnName(dynamicColumn.Key) is { } colName) map.ExcelColumnName = colName; map.ExcelColumnIndex = dynamicColumn.Index; @@ -285,7 +290,7 @@ internal static MiniExcelColumnMapping GetColumnMappingFromDynamicConfiguration( if (dynamicColumn.IndexName is { } idxName) member.ExcelIndexName = idxName; - if (dynamicColumn.Name is { } colName) + if (dynamicColumn.GetColumnName(columnName) is { } colName) member.ExcelColumnName = colName; return member; diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs index de39a2a4..b00f523c 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs @@ -2,6 +2,7 @@ using ExcelDataReader; using MiniExcelLib.Core.Exceptions; using MiniExcelLib.OpenXml.Tests.Utils; +using MiniExcelLib.Tests.Common; using MiniExcelLib.Tests.Common.Utils; namespace MiniExcelLib.OpenXml.Tests; @@ -1217,22 +1218,15 @@ await Assert.ThrowsAsync(async () => } [Fact] - public async Task ReadBigExcel_Prcoessing_TakeCancel_Throws_TaskCanceledException() + public async Task ReadBigExcel_Processing_TakeCancel_Throws_TaskCanceledException() { await Assert.ThrowsAsync(async () => { - var path = PathHelper.GetFile("xlsx/bigExcel.xlsx"); var cts = new CancellationTokenSource(); - _ = Task.Run(async () => - { - await Task.Delay(500); - await cts.CancelAsync(); - cts.Token.ThrowIfCancellationRequested(); - }); - - await using var stream = FileHelper.OpenRead(path); - _ = await _excelImporter.QueryAsync(stream, cancellationToken: cts.Token).ToListAsync(cts.Token); + var exportTask = _excelImporter.QueryAsync(PathHelper.GetFile("xlsx/bigExcel.xlsx"), cancellationToken: cts.Token).ToListAsync(cts.Token); + await cts.CancelAsync(); + await exportTask; }); } @@ -1924,4 +1918,105 @@ public async Task InvalidSheetNameCharactersShouldThrow() ms1.Seek(0, SeekOrigin.Begin); await Assert.ThrowsAsync(() => _excelExporter.AlterSheetAsync(ms3, "Sheet1", "Sheet*")); } + + class LocalizationSupportDto(string firstName, string lastName, string address, int age) + { + [MiniExcelColumn(Name = nameof(FirstName), ResourceType = typeof(Localization), Width = 15)] + public string? FirstName { get; set; } = firstName; + + [MiniExcelColumn(Name = nameof(LastName), ResourceType = typeof(Localization), Width = 15)] + public string? LastName { get; set; } = lastName; + + [MiniExcelColumnName("Address", ResourceType = typeof(Localization))] + public string? Residency { get; set; } = address; + + [MiniExcelColumn(Name = nameof(Age), ResourceType = typeof(Localization), Width = 20)] + public int Age { get; set; } = age; + } + + [Theory] + [InlineData("")] + [InlineData("it")] + [InlineData("zh")] + public async Task LocalizationTest(string cultureId) + { + var ogCulture = CultureInfo.CurrentUICulture; + + try + { + CultureInfo.CurrentUICulture = new CultureInfo(cultureId); + + await using var ms = new MemoryStream(); + await _excelExporter.ExportAsync(ms, Array.Empty()); + ms.Seek(0, SeekOrigin.Begin); + + using var package = new ExcelPackage(ms); + var cells = package.Workbook.Worksheets[0].Cells; + + var (firstName, lastName, address, age) = cultureId switch + { + "" => ("Name", "Surname", "Address", "Age"), + "it" => ("Nome", "Cognome", "Indirizzo", "Età"), + "zh" => ("名", "姓", "地址", "年龄"), + _ => throw new UnreachableException() + }; + + Assert.Equal(firstName, cells["A1"].Value); + Assert.Equal(lastName, cells["B1"].Value); + Assert.Equal(address, cells["C1"].Value); + Assert.Equal(age, cells["D1"].Value); + } + finally + { + CultureInfo.CurrentUICulture = ogCulture; + } + } + + [Theory] + [InlineData("")] + [InlineData("it")] + [InlineData("zh")] + public async Task LocalizationTestDynamicColumns(string cultureId) + { + var ogCulture = CultureInfo.CurrentUICulture; + + try + { + CultureInfo.CurrentUICulture = new CultureInfo(cultureId); + + DynamicExcelColumn[] cols = [ + new("FirstName") { ResourceType = typeof(Localization) }, + new("LastName") { ResourceType = typeof(Localization) }, + new("Address") { ResourceType = typeof(Localization) }, + new("Age") { ResourceType = typeof(Localization) } + ]; + + await using var stream = new MemoryStream(); + await _excelExporter.ExportAsync( + stream, + new[] { new { FirstName = "", LastName = "", Address = "", Age = 0 } }, + configuration: new OpenXmlConfiguration { DynamicColumns = cols }); + + stream.Seek(0, SeekOrigin.Begin); + using var package = new ExcelPackage(stream); + var cells = package.Workbook.Worksheets[0].Cells; + + var (firstName, lastName, address, age) = cultureId switch + { + "" => ("Name", "Surname", "Address", "Age"), + "it" => ("Nome", "Cognome", "Indirizzo", "Età"), + "zh" => ("名", "姓", "地址", "年龄"), + _ => throw new UnreachableException() + }; + + Assert.Equal(firstName, cells["A1"].Value); + Assert.Equal(lastName, cells["B1"].Value); + Assert.Equal(address, cells["C1"].Value); + Assert.Equal(age, cells["D1"].Value); + } + finally + { + CultureInfo.CurrentUICulture = ogCulture; + } + } } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs index 14b663be..db513c08 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs @@ -2,6 +2,7 @@ using ExcelDataReader; using MiniExcelLib.Core.Exceptions; using MiniExcelLib.OpenXml.Tests.Utils; +using MiniExcelLib.Tests.Common; using MiniExcelLib.Tests.Common.Utils; using FileHelper = MiniExcelLib.OpenXml.Tests.Utils.FileHelper; using Path = System.IO.Path; @@ -1730,4 +1731,105 @@ public async Task InvalidSheetNameCharactersShouldThrow() ms1.Seek(0, SeekOrigin.Begin); Assert.Throws(() => _excelExporter.AlterSheet(ms3, "Sheet1", "Sheet*")); } + + class LocalizationSupportDto(string firstName, string lastName, string address, int age) + { + [MiniExcelColumn(Name = nameof(FirstName), ResourceType = typeof(Localization), Width = 15)] + public string? FirstName { get; set; } = firstName; + + [MiniExcelColumn(Name = nameof(LastName), ResourceType = typeof(Localization), Width = 15)] + public string? LastName { get; set; } = lastName; + + [MiniExcelColumnName("Address", ResourceType = typeof(Localization))] + public string? Residency { get; set; } = address; + + [MiniExcelColumn(Name = nameof(Age), ResourceType = typeof(Localization), Width = 20)] + public int Age { get; set; } = age; + } + + [Theory] + [InlineData("")] + [InlineData("it")] + [InlineData("zh")] + public void LocalizationTest(string cultureId) + { + var ogCulture = CultureInfo.CurrentUICulture; + + try + { + CultureInfo.CurrentUICulture = new CultureInfo(cultureId); + + using var ms = new MemoryStream(); + _excelExporter.Export(ms, Array.Empty()); + ms.Seek(0, SeekOrigin.Begin); + + using var package = new ExcelPackage(ms); + var cells = package.Workbook.Worksheets[0].Cells; + + var (firstName, lastName, address, age) = cultureId switch + { + "" => ("Name", "Surname", "Address", "Age"), + "it" => ("Nome", "Cognome", "Indirizzo", "Età"), + "zh" => ("名", "姓", "地址", "年龄"), + _ => throw new UnreachableException() + }; + + Assert.Equal(firstName, cells["A1"].Value); + Assert.Equal(lastName, cells["B1"].Value); + Assert.Equal(address, cells["C1"].Value); + Assert.Equal(age, cells["D1"].Value); + } + finally + { + CultureInfo.CurrentUICulture = ogCulture; + } + } + + [Theory] + [InlineData("")] + [InlineData("it")] + [InlineData("zh")] + public void LocalizationTestDynamicColumns(string cultureId) + { + var ogCulture = CultureInfo.CurrentUICulture; + + try + { + CultureInfo.CurrentUICulture = new CultureInfo(cultureId); + + DynamicExcelColumn[] cols = [ + new("FirstName") { ResourceType = typeof(Localization) }, + new("LastName") { ResourceType = typeof(Localization) }, + new("Address") { ResourceType = typeof(Localization) }, + new("Age") { ResourceType = typeof(Localization) } + ]; + + using var stream = new MemoryStream(); + _excelExporter.Export( + stream, + new[] { new { FirstName = "", LastName = "", Address = "", Age = 0 } }, + configuration: new OpenXmlConfiguration { DynamicColumns = cols }); + + stream.Seek(0, SeekOrigin.Begin); + using var package = new ExcelPackage(stream); + var cells = package.Workbook.Worksheets[0].Cells; + + var (firstName, lastName, address, age) = cultureId switch + { + "" => ("Name", "Surname", "Address", "Age"), + "it" => ("Nome", "Cognome", "Indirizzo", "Età"), + "zh" => ("名", "姓", "地址", "年龄"), + _ => throw new UnreachableException() + }; + + Assert.Equal(firstName, cells["A1"].Value); + Assert.Equal(lastName, cells["B1"].Value); + Assert.Equal(address, cells["C1"].Value); + Assert.Equal(age, cells["D1"].Value); + } + finally + { + CultureInfo.CurrentUICulture = ogCulture; + } + } } diff --git a/tests/MiniExcel.Tests.Common/Localization.Designer.cs b/tests/MiniExcel.Tests.Common/Localization.Designer.cs new file mode 100644 index 00000000..4d082a59 --- /dev/null +++ b/tests/MiniExcel.Tests.Common/Localization.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace MiniExcelLib.Tests.Common { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class Localization { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Localization() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("MiniExcelLib.Tests.Common.Localization", typeof(Localization).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Address. + /// + public static string Address { + get { + return ResourceManager.GetString("Address", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Age. + /// + public static string Age { + get { + return ResourceManager.GetString("Age", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name. + /// + public static string FirstName { + get { + return ResourceManager.GetString("FirstName", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Surname. + /// + public static string LastName { + get { + return ResourceManager.GetString("LastName", resourceCulture); + } + } + } +} diff --git a/tests/MiniExcel.Tests.Common/Localization.it.resx b/tests/MiniExcel.Tests.Common/Localization.it.resx new file mode 100644 index 00000000..f581056a --- /dev/null +++ b/tests/MiniExcel.Tests.Common/Localization.it.resx @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Nome + + + Cognome + + + Indirizzo + + + Età + + \ No newline at end of file diff --git a/tests/MiniExcel.Tests.Common/Localization.resx b/tests/MiniExcel.Tests.Common/Localization.resx new file mode 100644 index 00000000..29f0c5a2 --- /dev/null +++ b/tests/MiniExcel.Tests.Common/Localization.resx @@ -0,0 +1,33 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Name + + + Surname + + + Age + + + Address + + \ No newline at end of file diff --git a/tests/MiniExcel.Tests.Common/Localization.zh.resx b/tests/MiniExcel.Tests.Common/Localization.zh.resx new file mode 100644 index 00000000..ab119829 --- /dev/null +++ b/tests/MiniExcel.Tests.Common/Localization.zh.resx @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + + + + + + 地址 + + + 年龄 + + \ No newline at end of file diff --git a/tests/MiniExcel.Tests.Common/MiniExcel.Tests.Common.csproj b/tests/MiniExcel.Tests.Common/MiniExcel.Tests.Common.csproj index 67a7fb1a..190935a9 100644 --- a/tests/MiniExcel.Tests.Common/MiniExcel.Tests.Common.csproj +++ b/tests/MiniExcel.Tests.Common/MiniExcel.Tests.Common.csproj @@ -8,8 +8,23 @@ - + + + + PublicResXFileCodeGenerator + Localization.Designer.cs + + + + + + True + True + Localization.resx + + +