Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
8 changes: 4 additions & 4 deletions .ai/examples/payment-plugin-walkthrough.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static class CashOnDeliveryPaymentDefaults
Three constants, referenced everywhere else:

- `ProviderSystemName` → the manifest's `SystemName`, the provider's `SystemName`, and the output folder. All three must agree; the system name is the plugin's persisted identity and cannot change after release.
- `FriendlyName` is a **resource key**, not a display string. The provider resolves it through `ITranslationService`.
- `FriendlyName` is a **resource key**, not a display string. The provider resolves it through `IRequestTranslationService`.
- `ConfigurationUrl` must match the admin controller's actual route, or the admin's *Configure* link 404s.

The manifest points back at the constant:
Expand All @@ -67,7 +67,7 @@ The manifest points back at the constant:
public class CashOnDeliveryPaymentProvider : IPaymentProvider
{
public CashOnDeliveryPaymentProvider(
ITranslationService translationService,
IRequestTranslationService translationService,
IHttpContextAccessor httpContextAccessor,
CashOnDeliveryPaymentSettings cashOnDeliveryPaymentSettings)
{ … }
Expand All @@ -85,7 +85,7 @@ public class CashOnDeliveryPaymentProvider : IPaymentProvider
Points worth copying:

- **The settings class is injected directly.** GrandNode registers `ISettings` implementations in DI — do not resolve them through `ISettingService` inside a provider.
- **`FriendlyName` goes through `ITranslationService`.** A literal here cannot be translated and cannot be changed by an operator.
- **`FriendlyName` goes through `IRequestTranslationService`.** A literal here cannot be translated and cannot be changed by an operator.
- **`Priority` comes from `DisplayOrder`**, so operators control ordering from the admin screen.
- **`LimitedToStores` / `LimitedToGroups`** are the scoping hooks from `.ai/knowledge/scoping.md`. Returning empty lists means "available everywhere".

Expand Down Expand Up @@ -206,6 +206,6 @@ A Release output path missing means the plugin silently disappears from release
1. Host starts → assembly scan finds `StartupApplication` → `IPaymentProvider` registered.
2. Plugin marked installed → `Install()` seeded settings and resource keys.
3. Checkout asks for available payment providers → this one appears, ordered by `Priority`, filtered by `LimitedToStores` / `LimitedToGroups`.
4. Its name renders through `FriendlyName` → `ITranslationService` → the resource key installed in step 2.
4. Its name renders through `FriendlyName` → `IRequestTranslationService` → the resource key installed in step 2.
5. Operator clicks *Configure* → `ConfigurationUrl` → admin controller → settings loaded for the active store.
6. Customer selects it → `ProcessPayment` → `TransactionStatus.Pending`.
4 changes: 2 additions & 2 deletions .ai/knowledge/dotnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ Use `switch` with `when` guards rather than nested `if/else`:
switch (cartItem.ShoppingCartTypeId)
{
case ShoppingCartType.ShoppingCart when product.DisableBuyButton:
context.AddFailure(translationService.GetResource("ShoppingCart.BuyingDisabled"));
context.AddFailure(translationService.GetResource("ShoppingCart.BuyingDisabled", value.Language.Id));
break;
case ShoppingCartType.Wishlist when product.DisableWishlistButton:
context.AddFailure(translationService.GetResource("ShoppingCart.WishlistDisabled"));
context.AddFailure(translationService.GetResource("ShoppingCart.WishlistDisabled", value.Language.Id));
break;
}
```
Expand Down
2 changes: 1 addition & 1 deletion .ai/knowledge/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class ShoppingCartStandardValidator : AbstractValidator<ShoppingCartStand
RuleFor(x => x).Custom((value, context) =>
{
if (!value.Product.Published)
context.AddFailure(translationService.GetResource("ShoppingCart.ProductUnpublished"));
context.AddFailure(translationService.GetResource("ShoppingCart.ProductUnpublished", value.Language.Id));
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion .ai/skills/plugin-payment.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Do not use this skill for order processing business logic outside the payment fl
- `TransactionStatus.Pending` — order placed but not yet confirmed.
- `TransactionStatus.Authorized` — gateway authorized; capture is separate.
- `TransactionStatus.Paid` — payment fully captured immediately.
4. Resolve `FriendlyName` through `ITranslationService.GetResource(Defaults.FriendlyName)`.
4. Resolve `FriendlyName` through `IRequestTranslationService.GetResource(Defaults.FriendlyName)`.
5. Set `LogoURL` to `"/Plugins/{SystemName}/logo.jpg"`.

#### Redirect Payment Flow
Expand Down
2 changes: 1 addition & 1 deletion .ai/skills/plugin-shipping.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Do not use this skill for shipping method domain data (managed via `IShippingMet
| `ShippingRateCalculationType` | `ShippingRateCalculationType.Off` for pre-configured rates; `ShippingRateCalculationType.Real` for live API rates. |
| `ShipmentTracker` | Return `null` if shipment tracking is not supported; return an `IShipmentTracker` implementation otherwise. |

3. Resolve `FriendlyName` through `ITranslationService.GetResource(Defaults.FriendlyName)` so it is localizable.
3. Resolve `FriendlyName` through `IRequestTranslationService.GetResource(Defaults.FriendlyName)` so it is localizable.
4. Resolve `Priority` from the plugin settings `DisplayOrder` property.
5. Use `IShippingMethodService.GetAllShippingMethods(restrictByCountryId, customer, storeId)` to iterate over admin-configured methods and build a `ShippingOption` for each.
6. Set `ShippingOption.Name` and `ShippingOption.Description` using `shippingMethod.GetTranslation(x => x.Name, languageId)`.
Expand Down
2 changes: 1 addition & 1 deletion .ai/skills/plugin-widget.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Do not use this skill for admin-only functionality with no storefront output, or
| `GetWidgetZones()` | Return the list of zone names where this widget renders. Use existing zone name constants from `Defaults` or string literals matching the zones used in storefront views. |
| `GetPublicViewComponentName(widgetZone)` | Return the view component name to invoke for the given zone. Can return different component names per zone, or the same name for all zones. |

3. Resolve `FriendlyName` through `ITranslationService.GetResource(Defaults.FriendlyName)`.
3. Resolve `FriendlyName` through `IRequestTranslationService.GetResource(Defaults.FriendlyName)`.
4. Resolve `Priority` from settings `DisplayOrder`.

#### View Component
Expand Down
4 changes: 2 additions & 2 deletions .ai/skills/settings-and-localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Do not use this skill as the primary review for MongoDB query safety or security
- `Plugin` (3) — plugin-owned resources.
- `Vendor` (5) — vendor panel.
14. Seed default resources for core features in `src/Modules/Grand.Module.Installer/Services/InstallDataLocaleResources.cs` via `src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml`.
15. Access resources in controllers and services by injecting `ITranslationService` and calling `GetResource(key)` or `GetResource(key, languageId, defaultValue)`.
15. Access resources by language. `ITranslationService` (Business) only has `GetResource(key, languageId, defaultValue)`: code in `src/Business` or the API module passes the language it has (the customer's, the order's, the store default). Web code that runs inside a request injects `IRequestTranslationService` (`Grand.Web.Common.Localization`), which adds `GetResource(key)` in the working language of the request.
16. Access resources in Razor views through the `Loc` indexer: `@Loc["key"]`. Do not call `ITranslationService` directly from views.
17. Translate enum values using `IEnumTranslationService` with keys formatted as `Enums.{TypeName}.{ValueName}`.

Expand Down Expand Up @@ -145,7 +145,7 @@ Include changed files, store-scope behavior, resource area classification, valid
- [ ] `DeleteSetting<T>` is called in plugin `Uninstall`.
- [ ] Every resource key added in `Install` is removed in `Uninstall`.
- [ ] Resource area is set correctly (`TranslationResourceArea`).
- [ ] Controllers use `ITranslationService.GetResource(key)` for display messages.
- [ ] Controllers use `IRequestTranslationService.GetResource(key)` for display messages; Business code passes a language.
- [ ] Views use `@Loc["key"]` and do not call `ITranslationService` directly.
- [ ] Entities that need per-language values implement `ITranslationEntity`.
- [ ] Admin models that expose localized fields implement `ILocalizedModel<T>`.
Expand Down
5 changes: 3 additions & 2 deletions .ai/templates/plugin/admin-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ using Grand.Domain.Permissions;
using Grand.Web.Common.Controllers;
using Grand.Web.Common.Filters;
using Grand.Web.Common.Helpers;
using Grand.Web.Common.Localization;
using Grand.Web.Common.Security.Authorization;
using Microsoft.AspNetCore.Mvc;
using {SystemName}.Models;
Expand All @@ -47,12 +48,12 @@ namespace {SystemName}.Areas.Admin.Controllers;
public class {ControllerName}Controller : Base{Kind}Controller
{
private readonly ISettingService _settingService;
private readonly ITranslationService _translationService;
private readonly IRequestTranslationService _translationService;
private readonly IAdminStoreService _adminStoreService;

public {ControllerName}Controller(
ISettingService settingService,
ITranslationService translationService,
IRequestTranslationService translationService,
IAdminStoreService adminStoreService)
{
_settingService = settingService;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Grand.Business.Core.Interfaces.Common.Localization;
using Grand.Data;
using Grand.Domain.Localization;
using Grand.Infrastructure;
using Grand.Infrastructure.Extensions;
using Grand.SharedKernel.Extensions;
using Grand.Mediator;
Expand All @@ -20,15 +19,12 @@ public class TranslationService : ITranslationService
/// <summary>
/// Ctor
/// </summary>
/// <param name="workContext">Work context</param>
/// <param name="trRepository">Translate resource repository</param>
/// <param name="mediator">Mediator</param>
public TranslationService(
IContextAccessor contextAccessor,
IRepository<TranslationResource> trRepository,
IMediator mediator)
{
_contextAccessor = contextAccessor;
_translationRepository = trRepository;
_mediator = mediator;
}
Expand All @@ -40,7 +36,6 @@ public TranslationService(
private static readonly ConcurrentDictionary<string, IDictionary<string, string>> _cachedResources = new();

private readonly IRepository<TranslationResource> _translationRepository;
private readonly IContextAccessor _contextAccessor;
private readonly IMediator _mediator;

#endregion
Expand Down Expand Up @@ -136,20 +131,6 @@ public virtual async Task DeleteTranslateResource(TranslationResource translateR
await _mediator.EntityDeleted(translateResource);
}

/// <summary>
/// Gets a resource string based on the specified ResourceKey property.
/// </summary>
/// <param name="name">A string representing a name.</param>
/// <returns>A string representing the requested resource string.</returns>
/// <remarks>
/// Reads the working language of the current request. Code that may run outside a request uses the
/// overload that takes the language.
/// </remarks>
public virtual string GetResource(string name)
{
return _contextAccessor.WorkContext.WorkingLanguage != null ? GetResource(name, _contextAccessor.WorkContext.WorkingLanguage.Id) : "";
}

/// <summary>
/// Gets a resource string based on the specified ResourceKey property.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,6 @@ public interface ITranslationService
/// <param name="translateResource">Translate resource</param>
Task DeleteTranslateResource(TranslationResource translateResource);

/// <summary>
/// Gets a resource string based on the specified ResourceKey property.
/// </summary>
/// <param name="name">A string representing a name.</param>
/// <returns>A string representing the requested resource string.</returns>
/// <remarks>
/// Reads the working language of the current request. Code that may run outside a request uses the
/// overload that takes the language.
/// </remarks>
string GetResource(string name);

/// <summary>
/// Gets a resource string based on the specified ResourceKey property.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using Grand.Infrastructure;

namespace Grand.Module.Api.Extensions;

public static class ContextAccessorExtensions
{
/// <summary>
/// Gets the working language id of the current request, or an empty string when none is resolved
/// </summary>
public static string WorkingLanguageId(this IContextAccessor contextAccessor)
{
return contextAccessor.WorkContext?.WorkingLanguage?.Id ?? string.Empty;
}
}
15 changes: 10 additions & 5 deletions src/Modules/Grand.Module.Api/Validators/Catalog/BrandValidator.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
using FluentValidation;
using Grand.Module.Api.Extensions;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Business.Core.Interfaces.Catalog.Brands;
using Grand.Business.Core.Interfaces.Common.Localization;
using Grand.Business.Core.Interfaces.Storage;
using Grand.Infrastructure;
using Grand.Infrastructure.Validators;

namespace Grand.Module.Api.Validators.Catalog;

public class BrandValidator : BaseGrandValidator<BrandDto>
{
public BrandValidator(IEnumerable<IValidatorConsumer<BrandDto>> validators,
ITranslationService translationService, IPictureService pictureService, IBrandService brandService,
ITranslationService translationService,
IContextAccessor contextAccessor, IPictureService pictureService, IBrandService brandService,
IBrandLayoutService brandLayoutService)
: base(validators)
{
var languageId = contextAccessor.WorkingLanguageId();

RuleFor(x => x.Name).NotEmpty()
.WithMessage(translationService.GetResource("Api.Catalog.Brand.Fields.Name.Required"));
.WithMessage(translationService.GetResource("Api.Catalog.Brand.Fields.Name.Required", languageId));
RuleFor(x => x).MustAsync(async (x, _, _) =>
{
if (!string.IsNullOrEmpty(x.PictureId))
Expand All @@ -26,7 +31,7 @@ public BrandValidator(IEnumerable<IValidatorConsumer<BrandDto>> validators,
}

return true;
}).WithMessage(translationService.GetResource("Api.Catalog.Brand.Fields.PictureId.NotExists"));
}).WithMessage(translationService.GetResource("Api.Catalog.Brand.Fields.PictureId.NotExists", languageId));

RuleFor(x => x).MustAsync(async (x, _, _) =>
{
Expand All @@ -38,7 +43,7 @@ public BrandValidator(IEnumerable<IValidatorConsumer<BrandDto>> validators,
}

return true;
}).WithMessage(translationService.GetResource("Api.Catalog.Brand.Fields.BrandLayoutId.NotExists"));
}).WithMessage(translationService.GetResource("Api.Catalog.Brand.Fields.BrandLayoutId.NotExists", languageId));

RuleFor(x => x).MustAsync(async (x, _, _) =>
{
Expand All @@ -50,6 +55,6 @@ public BrandValidator(IEnumerable<IValidatorConsumer<BrandDto>> validators,
}

return true;
}).WithMessage(translationService.GetResource("Api.Catalog.Brand.Fields.Id.NotExists"));
}).WithMessage(translationService.GetResource("Api.Catalog.Brand.Fields.Id.NotExists", languageId));
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
using FluentValidation;
using Grand.Module.Api.Extensions;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Business.Core.Interfaces.Catalog.Categories;
using Grand.Business.Core.Interfaces.Common.Localization;
using Grand.Business.Core.Interfaces.Storage;
using Grand.Infrastructure;
using Grand.Infrastructure.Validators;

namespace Grand.Module.Api.Validators.Catalog;

public class CategoryValidator : BaseGrandValidator<CategoryDto>
{
public CategoryValidator(IEnumerable<IValidatorConsumer<CategoryDto>> validators,
ITranslationService translationService, IPictureService pictureService, ICategoryService categoryService,
ITranslationService translationService,
IContextAccessor contextAccessor, IPictureService pictureService, ICategoryService categoryService,
ICategoryLayoutService
categoryLayoutService) : base(validators)
{
var languageId = contextAccessor.WorkingLanguageId();

RuleFor(x => x.Name).NotEmpty()
.WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.Name.Required"));
.WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.Name.Required", languageId));
RuleFor(x => x).MustAsync(async (x, _, _) =>
{
if (!string.IsNullOrEmpty(x.PictureId))
Expand All @@ -26,7 +31,7 @@ public CategoryValidator(IEnumerable<IValidatorConsumer<CategoryDto>> validators
}

return true;
}).WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.PictureId.NotExists"));
}).WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.PictureId.NotExists", languageId));

RuleFor(x => x).MustAsync(async (x, _, _) =>
{
Expand All @@ -38,7 +43,7 @@ public CategoryValidator(IEnumerable<IValidatorConsumer<CategoryDto>> validators
}

return true;
}).WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.ParentCategoryId.NotExists"));
}).WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.ParentCategoryId.NotExists", languageId));

RuleFor(x => x).MustAsync(async (x, _, _) =>
{
Expand All @@ -50,7 +55,7 @@ public CategoryValidator(IEnumerable<IValidatorConsumer<CategoryDto>> validators
}

return true;
}).WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.CategoryLayoutId.NotExists"));
}).WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.CategoryLayoutId.NotExists", languageId));

RuleFor(x => x).MustAsync(async (x, _, _) =>
{
Expand All @@ -62,6 +67,6 @@ public CategoryValidator(IEnumerable<IValidatorConsumer<CategoryDto>> validators
}

return true;
}).WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.Id.NotExists"));
}).WithMessage(translationService.GetResource("Api.Catalog.Category.Fields.Id.NotExists", languageId));
}
}
Loading
Loading