diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/HeadersDisplayNamesOptions.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/HeadersDisplayNamesOptions.cs new file mode 100644 index 000000000..a9772531b --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/HeadersDisplayNamesOptions.cs @@ -0,0 +1,12 @@ +namespace OrchardCore.Commerce.Abstractions; + +public class HeadersDisplayNamesOptions +{ + public string Price { get; set; } = "Price"; + public string NetPrice { get; set; } = "Net Price"; + public string GrossPrice { get; set; } = "Gross Price"; + public string OldPrice { get; set; } = "Old Price"; + public string Product { get; set; } = "Product"; + public string Quantity { get; set; } = "Quantity"; + public string Action { get; set; } = "Action"; +} diff --git a/src/Libraries/OrchardCore.Commerce.Abstractions/Helpers/LocalizationHelpers.cs b/src/Libraries/OrchardCore.Commerce.Abstractions/Helpers/LocalizationHelpers.cs new file mode 100644 index 000000000..e6814d8bb --- /dev/null +++ b/src/Libraries/OrchardCore.Commerce.Abstractions/Helpers/LocalizationHelpers.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc.Localization; +using OrchardCore.Commerce.Abstractions.Abstractions; +using System.Collections.Generic; + +namespace OrchardCore.Commerce.Abstractions.Helpers; + +public static class LocalizationHelpers +{ + public static IList GetLocalizedShoppingCartHeaders(IHtmlLocalizer htmlLocalizer, HeadersDisplayNamesOptions options) => + [ + htmlLocalizer[options.Product], + htmlLocalizer[options.Quantity], + htmlLocalizer[options.Price], + htmlLocalizer[options.Action], + ]; + + /// + /// Gets the localized string for Net Price. + /// + public static LocalizedHtmlString GetLocalizedNetPriceName( + IHtmlLocalizer htmlLocalizer, + HeadersDisplayNamesOptions options) => + htmlLocalizer[options.NetPrice]; + + /// + /// Gets the localized string for Gross Price. + /// + public static LocalizedHtmlString GetLocalizedGrossPriceName( + IHtmlLocalizer htmlLocalizer, + HeadersDisplayNamesOptions options) => + htmlLocalizer[options.GrossPrice]; + + /// + /// Formats a localized price label alongside an amount/total. + /// (e.g., "Net Price : $10.00") + /// + public static LocalizedHtmlString FormatPriceWithLabel( + IHtmlLocalizer htmlLocalizer, + string priceName, + object amount) => + htmlLocalizer["{0} : {1}", htmlLocalizer[priceName], amount]; +} diff --git a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/CheckoutStripe.cshtml b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/CheckoutStripe.cshtml index a818f505e..ce7037c56 100644 --- a/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/CheckoutStripe.cshtml +++ b/src/Modules/OrchardCore.Commerce.Payment.Stripe/Views/CheckoutStripe.cshtml @@ -1,4 +1,10 @@ +@using Microsoft.Extensions.Options; +@using Microsoft.AspNetCore.Mvc.Localization +@using OrchardCore.Commerce.Abstractions +@using OrchardCore.Commerce.Abstractions.Helpers @using OrchardCore.Commerce.MoneyDataType +@inject IOptions options; +@inject IHtmlLocalizer HtmlLocalizer; @{ if (Model.DefaultTotal is not Amount defaultTotal) @@ -19,12 +25,12 @@ @* Net Price is always applicable, while Gross Price can only be shown if it's specified. *@
- @T["Net Price: {0}", Model.NetTotal] + @LocalizationHelpers.FormatPriceWithLabel(HtmlLocalizer, options.Value.NetPrice, Model.NetTotal)
@if (Model.GrossTotal != null && Model.GrossTotal.Value != 0) {
- @T["Gross Price: {0}", Model.GrossTotal] + @LocalizationHelpers.FormatPriceWithLabel(HtmlLocalizer, options.Value.GrossPrice, Model.GrossTotal)
} diff --git a/src/Modules/OrchardCore.Commerce.Tax/Migrations/TaxPartMigrations.cs b/src/Modules/OrchardCore.Commerce.Tax/Migrations/TaxPartMigrations.cs index 7bc92fcc7..6251b2c99 100644 --- a/src/Modules/OrchardCore.Commerce.Tax/Migrations/TaxPartMigrations.cs +++ b/src/Modules/OrchardCore.Commerce.Tax/Migrations/TaxPartMigrations.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Options; +using OrchardCore.Commerce.Abstractions; using OrchardCore.Commerce.ContentFields.Settings; using OrchardCore.Commerce.Tax.Models; using OrchardCore.ContentFields.Settings; @@ -13,10 +15,14 @@ namespace OrchardCore.Commerce.Tax.Migrations; /// public class TaxPartMigrations : DataMigration { + private readonly HeadersDisplayNamesOptions _options; private readonly IContentDefinitionManager _contentDefinitionManager; - public TaxPartMigrations(IContentDefinitionManager contentDefinitionManager) => + public TaxPartMigrations(IContentDefinitionManager contentDefinitionManager, IOptions priceDisplayNameOptions) + { _contentDefinitionManager = contentDefinitionManager; + _options = priceDisplayNameOptions.Value; + } public async Task CreateAsync() { @@ -33,7 +39,7 @@ await _contentDefinitionManager Hint = "The ID that identifies the product of product category for taxing purposes.", })) .WithField(part => part.GrossPrice, part => part - .WithDisplayName("Gross Price") + .WithDisplayName(_options.GrossPrice) .WithSettings(new PriceFieldSettings { Hint = "The price with tax. If specified along with the Tax Rate, then Price content part is " + @@ -43,7 +49,7 @@ await _contentDefinitionManager .WithDisplayName("Tax Rate") .WithSettings(new NumericFieldSettings { - Hint = "The tax percentage of the net price, which is added to get the gross price.", + Hint = $"The tax percentage of the {_options.NetPrice}, which is added to get the {_options.GrossPrice}.", Minimum = 0, Maximum = 100, })) diff --git a/src/Modules/OrchardCore.Commerce/CommerceConstants.cs b/src/Modules/OrchardCore.Commerce/CommerceConstants.cs index 298696fa4..e38445cec 100644 --- a/src/Modules/OrchardCore.Commerce/CommerceConstants.cs +++ b/src/Modules/OrchardCore.Commerce/CommerceConstants.cs @@ -10,5 +10,6 @@ public static class Features public const string CurrencySettingsSelector = $"{Core}.{nameof(CurrencySettingsSelector)}"; public const string Subscription = $"{Core}.{nameof(Subscription)}"; public const string SkuGeneratorGuid = $"{SkuGenerator}.Guid"; + public const string HeadersDisplayNames = $"{Core}.{nameof(HeadersDisplayNames)}"; } } diff --git a/src/Modules/OrchardCore.Commerce/Events/PromotionShoppingCartEvents.cs b/src/Modules/OrchardCore.Commerce/Events/PromotionShoppingCartEvents.cs index 7dfcb8bc5..92957e8db 100644 --- a/src/Modules/OrchardCore.Commerce/Events/PromotionShoppingCartEvents.cs +++ b/src/Modules/OrchardCore.Commerce/Events/PromotionShoppingCartEvents.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Options; using OrchardCore.Commerce.Abstractions; using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.Models; @@ -17,6 +18,7 @@ public class PromotionShoppingCartEvents : ShoppingCartEventsBase private readonly IClock _clock; private readonly IHtmlLocalizer H; private readonly IPromotionService _promotionService; + private readonly HeadersDisplayNamesOptions _priceDisplayNameOptions; // Promotions should be applied after taxes. public override int Order => int.MaxValue; @@ -24,11 +26,13 @@ public class PromotionShoppingCartEvents : ShoppingCartEventsBase public PromotionShoppingCartEvents( IClock clock, IHtmlLocalizer htmlLocalizer, + IOptions priceDisplayNameOptions, IPromotionService promotionService) { _clock = clock; H = htmlLocalizer; _promotionService = promotionService; + _priceDisplayNameOptions = priceDisplayNameOptions.Value; } public override async Task<(IList Headers, IList Lines)> DisplayingAsync( @@ -50,12 +54,12 @@ public PromotionShoppingCartEvents( var newHeaders = headers.ToList(); - var netPriceExists = newHeaders.Exists(header => header.Name == "Net Price"); + var netPriceExists = newHeaders.Exists(header => header.Name == _priceDisplayNameOptions.NetPrice); var insertIndex = netPriceExists - ? newHeaders.FindIndex(header => header.Name == "Net Price") - : newHeaders.FindIndex(header => header.Name is "Price" or "Gross Price"); + ? newHeaders.FindIndex(header => header.Name == _priceDisplayNameOptions.NetPrice) + : newHeaders.FindIndex(header => header.Name == _priceDisplayNameOptions.Price || header.Name == _priceDisplayNameOptions.GrossPrice); - newHeaders.Insert(insertIndex, H["Old Price"]); + newHeaders.Insert(insertIndex, H[_priceDisplayNameOptions.OldPrice]); foreach (var (price, index) in lines.Select((item, index) => (item.UnitPrice, index))) { diff --git a/src/Modules/OrchardCore.Commerce/Events/TaxShoppingCartEvents.cs b/src/Modules/OrchardCore.Commerce/Events/TaxShoppingCartEvents.cs index 61b9cdfcb..54072caa8 100644 --- a/src/Modules/OrchardCore.Commerce/Events/TaxShoppingCartEvents.cs +++ b/src/Modules/OrchardCore.Commerce/Events/TaxShoppingCartEvents.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Options; using OrchardCore.Commerce.Abstractions; using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.Models; @@ -12,6 +13,7 @@ namespace OrchardCore.Commerce.Events; public class TaxShoppingCartEvents : ShoppingCartEventsBase { + private readonly HeadersDisplayNamesOptions _options; private readonly IHtmlLocalizer H; private readonly IEnumerable _taxProviders; private readonly ISiteService _siteService; @@ -19,10 +21,12 @@ public class TaxShoppingCartEvents : ShoppingCartEventsBase public override int Order => 0; public TaxShoppingCartEvents( + IOptions priceDisplayNameOptions, IHtmlLocalizer htmlLocalizer, IEnumerable taxProviders, ISiteService siteService) { + _options = priceDisplayNameOptions.Value; H = htmlLocalizer; _taxProviders = taxProviders; _siteService = siteService; @@ -60,15 +64,15 @@ public TaxShoppingCartEvents( } var newHeaders = headers - .Select(header => header.Name == "Price" ? H["Gross Price"] : header) + .Select(header => header.Name == _options.Price ? H[_options.GrossPrice] : header) .ToList(); // When taxes are specified, Gross Price is always applicable, while Net Price is optional. var priceDisplaySettings = (await _siteService.GetSiteSettingsAsync()).GetOrCreate(); if (priceDisplaySettings.UseNetPriceDisplay) { - var grossIndex = newHeaders.FindIndex(header => header.Name == "Gross Price"); - newHeaders.Insert(grossIndex, H["Net Price"]); + var grossIndex = newHeaders.FindIndex(header => header.Name == _options.GrossPrice); + newHeaders.Insert(grossIndex, H[_options.NetPrice]); } return (newHeaders, lines); diff --git a/src/Modules/OrchardCore.Commerce/Manifest.cs b/src/Modules/OrchardCore.Commerce/Manifest.cs index b7d28ca34..ea0993123 100644 --- a/src/Modules/OrchardCore.Commerce/Manifest.cs +++ b/src/Modules/OrchardCore.Commerce/Manifest.cs @@ -21,6 +21,7 @@ "OrchardCore.Templates", "OrchardCore.Commerce.ContentFields", "OrchardCore.Commerce.Payment", + CommerceConstants.Features.HeadersDisplayNames, ] )] @@ -70,3 +71,10 @@ CommerceConstants.Features.Core, ] )] + +[assembly: Feature( + Id = CommerceConstants.Features.HeadersDisplayNames, + Name = "Orchard Core Commerce - Display Names", + Category = "Commerce", + Description = "Allows configuring display names." +)] diff --git a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs index c19287a14..6d2ade61b 100644 --- a/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs +++ b/src/Modules/OrchardCore.Commerce/Services/ShoppingCartHelpers.cs @@ -1,8 +1,10 @@ using Lombiq.HelpfulLibraries.AspNetCore.Exceptions; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Localization; +using Microsoft.Extensions.Options; using OrchardCore.Commerce.Abstractions; using OrchardCore.Commerce.Abstractions.Abstractions; +using OrchardCore.Commerce.Abstractions.Helpers; using OrchardCore.Commerce.Abstractions.Models; using OrchardCore.Commerce.Abstractions.ViewModels; using OrchardCore.Commerce.AddressDataType; @@ -20,6 +22,7 @@ namespace OrchardCore.Commerce.Services; public class ShoppingCartHelpers : IShoppingCartHelpers { + private readonly HeadersDisplayNamesOptions _options; private readonly IHttpContextAccessor _hca; private readonly IPriceSelectionStrategy _priceSelectionStrategy; private readonly IPriceService _priceService; @@ -30,6 +33,7 @@ public class ShoppingCartHelpers : IShoppingCartHelpers private readonly IShoppingCartPersistence _shoppingCartPersistence; private readonly IShoppingCartSerializer _shoppingCartSerializer; private readonly IHtmlLocalizer H; + private readonly IHtmlLocalizer _htmlLocalizer; [SuppressMessage( "Major Code Smell", @@ -45,7 +49,9 @@ public ShoppingCartHelpers( IEnumerable shoppingCartEvents, IShoppingCartPersistence shoppingCartPersistence, IShoppingCartSerializer shoppingCartSerializer, - IHtmlLocalizer localizer) + IOptions priceDisplayNameOptions, + IHtmlLocalizer localizer, + IHtmlLocalizer htmlLocalizer) { _hca = hca; _priceSelectionStrategy = priceSelectionStrategy; @@ -56,7 +62,9 @@ public ShoppingCartHelpers( _shoppingCartEvents = shoppingCartEvents; _shoppingCartPersistence = shoppingCartPersistence; _shoppingCartSerializer = shoppingCartSerializer; + _options = priceDisplayNameOptions.Value; H = localizer; + _htmlLocalizer = htmlLocalizer; } public async Task CreateShoppingCartViewModelAsync( @@ -99,13 +107,8 @@ private async Task CreateShoppingCartViewModelAsync( if (lines.Count == 0) return null; - IList headers = - [ - H["Quantity"], - H["Product"], - H["Price"], - H["Action"], - ]; + var headers = LocalizationHelpers.GetLocalizedShoppingCartHeaders(_htmlLocalizer, _options); + IList totals = [.. (await CalculateMultipleCurrencyTotalsAsync(cart)).Values]; (shipping, billing) = await _hca.GetUserAddressIfNullAsync(shipping, billing); diff --git a/src/Modules/OrchardCore.Commerce/Startup.cs b/src/Modules/OrchardCore.Commerce/Startup.cs index 5c3bba3f5..f7f2a9b8c 100644 --- a/src/Modules/OrchardCore.Commerce/Startup.cs +++ b/src/Modules/OrchardCore.Commerce/Startup.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; @@ -44,6 +45,7 @@ using OrchardCore.Data.Migration; using OrchardCore.Deployment; using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.Environment.Shell.Configuration; using OrchardCore.Liquid; using OrchardCore.Modules; using OrchardCore.Mvc.Core.Utilities; @@ -436,3 +438,18 @@ public class SkuGeneratorGuidStartup : StartupBase public override void ConfigureServices(IServiceCollection services) => services.AddScoped(); } + +[RequireFeatures(CommerceConstants.Features.HeadersDisplayNames)] +public class PriceDisplayNamesStartup : StartupBase +{ + private readonly IShellConfiguration _shellConfiguration; + public PriceDisplayNamesStartup(IShellConfiguration shellConfiguration) => _shellConfiguration = shellConfiguration; + public override void ConfigureServices(IServiceCollection services) + { + var section = _shellConfiguration.GetSection("OrchardCoreCommerce_HeadersDisplayNames"); + if (section.Exists()) + { + services.Configure(section); + } + } +} diff --git a/src/Modules/OrchardCore.Commerce/Views/PriceDisplaySettings.Edit.cshtml b/src/Modules/OrchardCore.Commerce/Views/PriceDisplaySettings.Edit.cshtml index 9ae5eb658..cbff9b2f1 100644 --- a/src/Modules/OrchardCore.Commerce/Views/PriceDisplaySettings.Edit.cshtml +++ b/src/Modules/OrchardCore.Commerce/Views/PriceDisplaySettings.Edit.cshtml @@ -1,16 +1,18 @@ +@using Microsoft.Extensions.Options +@using OrchardCore.Commerce.Abstractions @model PriceDisplaySettingsViewModel - +@inject IOptions options;
- +
@T["Turn on to display prices with taxes excluded."]
- +
@T["Turn on to display prices with taxes included."] diff --git a/src/Modules/OrchardCore.Commerce/Views/ShoppingCartTable.cshtml b/src/Modules/OrchardCore.Commerce/Views/ShoppingCartTable.cshtml index 3fab53909..11e7b3d24 100644 --- a/src/Modules/OrchardCore.Commerce/Views/ShoppingCartTable.cshtml +++ b/src/Modules/OrchardCore.Commerce/Views/ShoppingCartTable.cshtml @@ -1,9 +1,15 @@ +@using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.Extensions.Options +@using OrchardCore.Commerce.Abstractions +@using OrchardCore.Commerce.Abstractions.Helpers @using OrchardCore.Commerce.Abstractions.ViewModels @using OrchardCore.Commerce.Models @using OrchardCore.Commerce.MoneyDataType @using OrchardCore.Commerce.Tax.Extensions @using OrchardCore.Settings @inject ISiteService SiteService +@inject IOptions options; +@inject IHtmlLocalizer HtmlLocalizer; @{ var viewModel = (ShoppingCartViewModel)Model.ShoppingCartViewModel; @@ -70,7 +76,7 @@

@if (priceDisplaySettings.UseNetPriceDisplay) { - @T["Net Price: {0}", netTotal] + @LocalizationHelpers.FormatPriceWithLabel(HtmlLocalizer, options.Value.NetPrice, netTotal) } @if (priceDisplaySettings.UseNetPriceDisplay && priceDisplaySettings.UseGrossPriceDisplay) { @@ -78,7 +84,7 @@ } @if (priceDisplaySettings.UseGrossPriceDisplay) { - @T["Gross Price: {0}", grossTotal] + @LocalizationHelpers.FormatPriceWithLabel(HtmlLocalizer, options.Value.GrossPrice, grossTotal) }

} diff --git a/src/Modules/OrchardCore.Commerce/Views/TaxPart-GrossPrice.cshtml b/src/Modules/OrchardCore.Commerce/Views/TaxPart-GrossPrice.cshtml index 78964eb1c..1f86dd679 100644 --- a/src/Modules/OrchardCore.Commerce/Views/TaxPart-GrossPrice.cshtml +++ b/src/Modules/OrchardCore.Commerce/Views/TaxPart-GrossPrice.cshtml @@ -1,11 +1,13 @@ +@using Microsoft.Extensions.Options +@using OrchardCore.Commerce.Abstractions @using OrchardCore.Commerce.Drivers @model OrchardCore.Commerce.ContentFields.ViewModels.PriceFieldDisplayViewModel - +@inject IOptions options; @{ if (!Model.Field.Amount.IsValid || Context.Items[nameof(TaxRateTaxPartDisplayDriver)] is true) { return; } }
- @T["Gross Price:"] + @T[options.Value.GrossPrice] @Model.Field.Amount
diff --git a/src/Modules/OrchardCore.Commerce/Views/TaxPart_TaxRate_GrossPrice.cshtml b/src/Modules/OrchardCore.Commerce/Views/TaxPart_TaxRate_GrossPrice.cshtml index 172927e12..b4905c84f 100644 --- a/src/Modules/OrchardCore.Commerce/Views/TaxPart_TaxRate_GrossPrice.cshtml +++ b/src/Modules/OrchardCore.Commerce/Views/TaxPart_TaxRate_GrossPrice.cshtml @@ -1,10 +1,16 @@ +@using Microsoft.AspNetCore.Mvc.Localization +@using Microsoft.Extensions.Options +@using OrchardCore.Commerce.Abstractions +@using OrchardCore.Commerce.Abstractions.Helpers @model TaxRateViewModel +@inject IOptions options; +@inject IHtmlLocalizer HtmlLocalizer; @if (Model.GrossPrice is { } price) {
- @T["Gross Price*:"] + @LocalizationHelpers.GetLocalizedGrossPriceName(HtmlLocalizer, options.Value): @price
} diff --git a/src/OrchardCore.Commerce.Web/appsettings.json b/src/OrchardCore.Commerce.Web/appsettings.json index 51c11b243..16177bdd8 100644 --- a/src/OrchardCore.Commerce.Web/appsettings.json +++ b/src/OrchardCore.Commerce.Web/appsettings.json @@ -5,5 +5,15 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "OrchardCore": { + "OrchardCoreCommerce_HeadersDisplayNames": { + "Price": "Price", + "NetPrice": "Net Price", + "GrossPrice": "Gross Price", + "OldPrice": "Old Price", + "Product": "Product", + "Quantity": "Quantity", + "Action": "Action" + } + } }