diff --git a/sample/Directory.Packages.props b/sample/Directory.Packages.props index 5204db763..ebfb96c1a 100644 --- a/sample/Directory.Packages.props +++ b/sample/Directory.Packages.props @@ -8,6 +8,7 @@ + @@ -28,6 +29,7 @@ + diff --git a/sample/src/NimblePros.SampleToDo.Core/Localization.cs b/sample/src/NimblePros.SampleToDo.Core/Localization.cs new file mode 100644 index 000000000..33839718d --- /dev/null +++ b/sample/src/NimblePros.SampleToDo.Core/Localization.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Localization; + +/// +/// Exposes the current to static code +/// (Vogen Validate, domain events, etc.) without pulling the whole +/// DI container into Core. +/// +public static class Localization +{ + /// + /// Set by Program.cs during app startup. + /// + public static ILocalizationContext? Current { get; set; } + + public sealed class LocalizationContext : ILocalizationContext + { + public IStringLocalizer Localizer { get; } + + public LocalizationContext(IStringLocalizer localizer) => + Localizer = localizer; + } +} + +public interface ILocalizationContext +{ + IStringLocalizer Localizer { get; } +} diff --git a/sample/src/NimblePros.SampleToDo.Core/NimblePros.SampleToDo.Core.csproj b/sample/src/NimblePros.SampleToDo.Core/NimblePros.SampleToDo.Core.csproj index 3eb6be895..584d46756 100644 --- a/sample/src/NimblePros.SampleToDo.Core/NimblePros.SampleToDo.Core.csproj +++ b/sample/src/NimblePros.SampleToDo.Core/NimblePros.SampleToDo.Core.csproj @@ -12,6 +12,7 @@ + diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Project.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Project.cs index ccf4c6939..1884fdf55 100644 --- a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Project.cs +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Project.cs @@ -1,4 +1,5 @@ -using NimblePros.SampleToDo.Core.ProjectAggregate.Events; +using NimblePros.SampleToDo.Core.ContributorAggregate.Events; +using NimblePros.SampleToDo.Core.ProjectAggregate.Events; namespace NimblePros.SampleToDo.Core.ProjectAggregate; @@ -27,6 +28,7 @@ public Project AddItem(ToDoItem newItem) public Project UpdateName(ProjectName newName) { + if (Name.Equals(newName)) return this; Name = newName; return this; } diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectErrorMessages.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectErrorMessages.cs new file mode 100644 index 000000000..002033427 --- /dev/null +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectErrorMessages.cs @@ -0,0 +1,53 @@ +// Core/ProjectAggregate/ProjectMessages.cs +namespace NimblePros.SampleToDo.Core.ProjectAggregate; + +/// +/// Strongly-typed, localisable message accessors for the Project aggregate. +/// The implementation is deliberately **static** so it can be called from Vogen’s +/// static Validate method. +/// +public static class ProjectErrorMessages +{ + // ----------------------------------------------------------------- + // 1. Public entry points – these are what you call from the domain + // ----------------------------------------------------------------- + public static string CoreProjectNameEmpty => Get(nameof(CoreProjectNameEmpty)); + public static string CoreProjectNameTooLong(int maxLength) => Get(nameof(CoreProjectNameTooLong), maxLength); + public static string CoreToDoItemDescriptionEmpty => Get(nameof(CoreToDoItemDescriptionEmpty)); + public static string CoreToDoItemDescriptionTooLong(int maxLength) => Get(nameof(CoreToDoItemDescriptionTooLong), maxLength); + public static string CoreToDoItemTitleEmpty => Get(nameof(CoreToDoItemTitleEmpty)); + public static string CoreToDoItemTitleTooLong(int maxLength) => Get(nameof(CoreToDoItemTitleTooLong), maxLength); + + // ----------------------------------------------------------------- + // 2. Private helper that forwards to the current localizer + // ----------------------------------------------------------------- + private static string Get(string key, params object[] args) + { + // The static holder is set once in Program.cs (Web project) + var localizer = Localization.Current?.Localizer; + + if (localizer is not null) + { + // Uses the standard {0}, {1}… placeholders defined in JSON/RESX + var localized = localizer[key, args]; + return localized.ResourceNotFound ? Fallback(key, args) : localized; + } + + // No DI container available (e.g. unit-tests) → fallback to English + return Fallback(key, args); + } + + // ----------------------------------------------------------------- + // 3. Hard-coded English fallback (never throws, always returns a string) + // ----------------------------------------------------------------- + private static string Fallback(string key, object[] args) => key switch + { + nameof(CoreProjectNameEmpty) => "Name cannot be empty", + nameof(CoreProjectNameTooLong) => FormattableString.Invariant($"Name cannot be longer than {args[0]} characters"), + nameof(CoreToDoItemDescriptionEmpty) => "Description cannot be empty", + nameof(CoreToDoItemDescriptionTooLong) => FormattableString.Invariant($"Description cannot be longer than {args[0]} characters"), + nameof(CoreToDoItemTitleEmpty) => "Title cannot be empty", + nameof(CoreToDoItemTitleTooLong) => FormattableString.Invariant($"Title cannot be longer than {args[0]} characters"), + _ => $"[{key}]" + }; +} diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectName.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectName.cs index ab7df0550..b15a564e4 100644 --- a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectName.cs +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ProjectName.cs @@ -13,8 +13,8 @@ public partial struct ProjectName public const int MaxLength = 100; private static Validation Validate(in string name) => string.IsNullOrEmpty(name) - ? Validation.Invalid("Name cannot be empty") + ? Validation.Invalid(ProjectErrorMessages.CoreProjectNameEmpty) : name.Length > MaxLength - ? Validation.Invalid($"Name cannot be longer than {MaxLength} characters") + ? Validation.Invalid(ProjectErrorMessages.CoreProjectNameTooLong(MaxLength)) : Validation.Ok; } diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSearchSpec.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSearchSpec.cs index 36185587b..3fc78916e 100644 --- a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSearchSpec.cs +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/Specifications/IncompleteItemsSearchSpec.cs @@ -6,7 +6,7 @@ public IncompleteItemsSearchSpec(string searchString) { Query .Where(item => !item.IsDone && - (item.Title.Contains(searchString) || - item.Description.Contains(searchString))); + (item.Title.Value.Contains(searchString) || + item.Description.Value.Contains(searchString))); } } diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItem.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItem.cs index 03dc54b87..e21fe7f29 100644 --- a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItem.cs +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItem.cs @@ -1,4 +1,5 @@ -using NimblePros.SampleToDo.Core.ContributorAggregate; +using System.Xml.Linq; +using NimblePros.SampleToDo.Core.ContributorAggregate; using NimblePros.SampleToDo.Core.ProjectAggregate.Events; namespace NimblePros.SampleToDo.Core.ProjectAggregate; @@ -8,14 +9,19 @@ public class ToDoItem : EntityBase public ToDoItem() : this(Priority.Backlog) { } + public ToDoItem(ToDoItemTitle title, ToDoItemDescription description) : this(Priority.Backlog) + { + Title = title; + Description = description; + } public ToDoItem(Priority priority) { Priority = priority; } - public string Title { get; set; } = string.Empty; // TODO: Use Value Object - public string Description { get; set; } = string.Empty; // TODO: Use Value Object + public ToDoItemTitle Title { get; private set; } + public ToDoItemDescription Description { get; private set; } public ContributorId? ContributorId { get; private set; } // tasks don't have anyone assigned when first created public bool IsDone { get; private set; } @@ -48,9 +54,23 @@ public ToDoItem RemoveContributor() return this; } + public ToDoItem UpdateTitle(ToDoItemTitle newTitle) + { + if (Title.Equals(newTitle)) return this; + Title = newTitle; + return this; + } + + public ToDoItem UpdateDescription(ToDoItemDescription newDescription) + { + if (Description.Equals(newDescription)) return this; + Description = newDescription; + return this; + } + public override string ToString() { string status = IsDone ? "Done!" : "Not done."; - return $"{Id}: Status: {status} - {Title} - Priority: {Priority}"; + return $"{Id}: Status: {status} - {Title.Value} - Priority: {Priority}"; } } diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemDescription.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemDescription.cs new file mode 100644 index 000000000..4460cdd7e --- /dev/null +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemDescription.cs @@ -0,0 +1,15 @@ +using Vogen; + +namespace NimblePros.SampleToDo.Core.ProjectAggregate; + +[ValueObject(conversions: Conversions.SystemTextJson)] +public partial struct ToDoItemDescription +{ + public const int MaxLength = 200; + private static Validation Validate(in string description) => + string.IsNullOrEmpty(description) + ? Validation.Invalid(ProjectErrorMessages.CoreToDoItemDescriptionEmpty) + : description.Length > MaxLength + ? Validation.Invalid(ProjectErrorMessages.CoreToDoItemDescriptionTooLong(MaxLength)) + : Validation.Ok; +} diff --git a/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemTitle.cs b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemTitle.cs new file mode 100644 index 000000000..e447fa1b0 --- /dev/null +++ b/sample/src/NimblePros.SampleToDo.Core/ProjectAggregate/ToDoItemTitle.cs @@ -0,0 +1,15 @@ +using Vogen; + +namespace NimblePros.SampleToDo.Core.ProjectAggregate; + +[ValueObject(conversions: Conversions.SystemTextJson)] +public partial struct ToDoItemTitle +{ + public const int MaxLength = 100; + private static Validation Validate(in string title) => + string.IsNullOrEmpty(title) + ? Validation.Invalid(ProjectErrorMessages.CoreToDoItemTitleEmpty) + : title.Length > MaxLength + ? Validation.Invalid(ProjectErrorMessages.CoreToDoItemTitleTooLong(MaxLength)) + : Validation.Ok; +} diff --git a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ToDoItemConfiguration.cs b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ToDoItemConfiguration.cs index dee312b12..07974d63b 100644 --- a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ToDoItemConfiguration.cs +++ b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/ToDoItemConfiguration.cs @@ -11,11 +11,17 @@ public void Configure(EntityTypeBuilder builder) .HasValueGenerator>() .HasVogenConversion() .IsRequired(); - builder.Property(t => t.Title) - .HasMaxLength(DataSchemaConstants.DEFAULT_NAME_LENGTH) + + builder.Property(p => p.Title) + .HasVogenConversion() + .HasMaxLength(ToDoItemTitle.MaxLength) + .IsRequired(); + + builder.Property(p => p.Description) + .HasVogenConversion() + .HasMaxLength(ToDoItemDescription.MaxLength) .IsRequired(); - builder.Property(t => t.Description) - .HasMaxLength(200); + builder.Property(t => t.ContributorId) .HasConversion( v => v.HasValue ? v.Value.Value : (int?)null, // to db diff --git a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/VogenEfCoreConverters.cs b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/VogenEfCoreConverters.cs index f07a894b2..9643b0ae4 100644 --- a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/VogenEfCoreConverters.cs +++ b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Config/VogenEfCoreConverters.cs @@ -9,4 +9,6 @@ namespace NimblePros.SampleToDo.Infrastructure.Data.Config; [EfCoreConverter] [EfCoreConverter] [EfCoreConverter] +[EfCoreConverter] +[EfCoreConverter] internal partial class VogenEfCoreConverters; diff --git a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListIncompleteItemsQueryService.cs b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListIncompleteItemsQueryService.cs index 7aa24553b..72c5d4017 100644 --- a/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListIncompleteItemsQueryService.cs +++ b/sample/src/NimblePros.SampleToDo.Infrastructure/Data/Queries/ListIncompleteItemsQueryService.cs @@ -17,7 +17,7 @@ public async Task> ListAsync(int projectId) var projectParameter = new SqlParameter("@projectId", System.Data.SqlDbType.Int); var result = await _db.ToDoItems.FromSqlRaw("SELECT Id, Title, Description, IsDone, ContributorId FROM ToDoItems WHERE ProjectId = @ProjectId", projectParameter) // don't fetch other big columns - .Select(x => new ToDoItemDto(x.Id, x.Title, x.Description, x.IsDone, x.ContributorId)) + .Select(x => new ToDoItemDto(x.Id, x.Title.Value, x.Description.Value, x.IsDone, x.ContributorId)) .ToListAsync(); return result; diff --git a/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemCommand.cs b/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemCommand.cs index 129c523d4..6f2f1d541 100644 --- a/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemCommand.cs +++ b/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemCommand.cs @@ -12,5 +12,5 @@ namespace NimblePros.SampleToDo.UseCases.Projects.AddToDoItem; /// public record AddToDoItemCommand(ProjectId ProjectId, ContributorId? ContributorId, - string Title, - string Description) : ICommand>; + ToDoItemTitle Title, + ToDoItemDescription Description) : ICommand>; diff --git a/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemHandler.cs b/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemHandler.cs index 6d14b45a7..5677022c3 100644 --- a/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemHandler.cs +++ b/sample/src/NimblePros.SampleToDo.UseCases/Projects/AddToDoItem/AddToDoItemHandler.cs @@ -22,13 +22,9 @@ public async ValueTask> Handle(AddToDoItemCommand request, return Result.NotFound(); } - var newItem = new ToDoItem() - { - Title = request.Title!, - Description = request.Description! - }; + var newItem = new ToDoItem(title: request.Title!, description: request.Description!); - if(request.ContributorId.HasValue) + if (request.ContributorId.HasValue) { newItem.AddContributor(request.ContributorId.Value); } diff --git a/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsHandler.cs b/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsHandler.cs index 446fb2e05..9f51a1aa4 100644 --- a/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsHandler.cs +++ b/sample/src/NimblePros.SampleToDo.UseCases/Projects/GetWithAllItems/GetProjectWithAllItemsHandler.cs @@ -22,7 +22,7 @@ public async ValueTask> Handle(GetProjectWithAllI if (project == null) return Result.NotFound(); var items = project.Items - .Select(i => new ToDoItemDto(i.Id, i.Title, i.Description, i.IsDone, i.ContributorId)).ToList(); + .Select(i => new ToDoItemDto(i.Id, i.Title.Value, i.Description.Value, i.IsDone, i.ContributorId)).ToList(); return new ProjectWithAllItemsDto(project.Id, project.Name, items, project.Status.ToString()) ; } diff --git a/sample/src/NimblePros.SampleToDo.Web/NimblePros.SampleToDo.Web.csproj b/sample/src/NimblePros.SampleToDo.Web/NimblePros.SampleToDo.Web.csproj index 4b55e3716..d39d46d85 100644 --- a/sample/src/NimblePros.SampleToDo.Web/NimblePros.SampleToDo.Web.csproj +++ b/sample/src/NimblePros.SampleToDo.Web/NimblePros.SampleToDo.Web.csproj @@ -1,6 +1,6 @@  - + true Exe @@ -10,11 +10,11 @@ True - + @@ -32,11 +32,11 @@ - + - + diff --git a/sample/src/NimblePros.SampleToDo.Web/Program.cs b/sample/src/NimblePros.SampleToDo.Web/Program.cs index 038d51a10..b4d320ec2 100644 --- a/sample/src/NimblePros.SampleToDo.Web/Program.cs +++ b/sample/src/NimblePros.SampleToDo.Web/Program.cs @@ -1,5 +1,9 @@ -using FluentValidation; +using System.Text; +using AspNetCore.Localizer.Json.Extensions; +using FluentValidation; +using Microsoft.Extensions.Localization; using NimblePros.Metronome; +using NimblePros.SampleToDo.Core.ProjectAggregate; using NimblePros.SampleToDo.Infrastructure.Data; using NimblePros.SampleToDo.Web.Configurations; using NimblePros.SampleToDo.Web.Projects; @@ -60,8 +64,39 @@ private static async Task Main(string[] args) // track db and external service calls builder.Services.AddMetronome(); + // Add localization with the JSON path + builder.Services.AddLocalization(options => options.ResourcesPath = "i18n"); + + // ----- Add JSON-specific localization + builder.Services.AddJsonLocalization(options => + { + options.ResourcesPath = "i18n"; // Path to the JSON files (e.g., i18n/en/Project.json) + options.CacheDuration = TimeSpan.FromHours(1); // Optional: Cache for performance + options.FileEncoding = Encoding.UTF8; //Optional: Specify file encoding + }); + + // ----- Register the typed localizer for the Project aggregate ----- + builder.Services.AddSingleton(sp => + { + var factory = sp.GetRequiredService(); + // Creates a localizer for 'ProjectAggregate' – loads from i18n/{culture}/Project.json + return factory.Create(typeof(ProjectErrorMessages)); + }); + var app = builder.Build(); - + + // ----- Set the static holder for Core access ----- + var localizer = app.Services.GetRequiredService(); + Localization.Current = new Localization.LocalizationContext(localizer); + + // ----- Add request localization middleware (before app.Run()) ----- + var supportedCultures = new[] { "en", "fr", "fa" }; // Add your cultures + var localizationOptions = new RequestLocalizationOptions() + .SetDefaultCulture(supportedCultures[0]) + .AddSupportedCultures(supportedCultures) + .AddSupportedUICultures(supportedCultures); + app.UseRequestLocalization(localizationOptions); + // Verify validators are registered properly in development if (app.Environment.IsDevelopment()) { diff --git a/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.cs b/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.cs index 54ab1c0b5..359c70616 100644 --- a/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.cs +++ b/sample/src/NimblePros.SampleToDo.Web/Projects/CreateToDoItem.cs @@ -1,7 +1,6 @@ using NimblePros.SampleToDo.Core.ContributorAggregate; using NimblePros.SampleToDo.Core.ProjectAggregate; using NimblePros.SampleToDo.UseCases.Projects.AddToDoItem; -using NimblePros.SampleToDo.Web.Extensions; using NimblePros.SampleToDo.Web.Projects; namespace NimblePros.SampleToDo.Web.ProjectEndpoints; @@ -30,17 +29,17 @@ public override void Configure() Title = "Implement user authentication", Description = "Add JWT-based authentication to the API" }; - + // Document possible responses s.Responses[201] = "Todo item created successfully"; s.Responses[404] = "Project or contributor not found"; s.Responses[400] = "Invalid input data"; s.Responses[500] = "Internal server error"; }); - + // Add tags for API grouping Tags("Projects"); - + // Add additional metadata Description(builder => builder .Accepts("application/json") @@ -57,7 +56,7 @@ public override async Task> ? ContributorId.From(request.ContributorId.Value) : null; var command = new AddToDoItemCommand(ProjectId.From(request.ProjectId), contributorId, - request.Title, request.Description); + ToDoItemTitle.From(request.Title), ToDoItemDescription.From(request.Description)); var result = await _mediator.Send(command); return result.Status switch diff --git a/sample/src/NimblePros.SampleToDo.Web/SeedData.cs b/sample/src/NimblePros.SampleToDo.Web/SeedData.cs index e609c982f..7f74379f3 100644 --- a/sample/src/NimblePros.SampleToDo.Web/SeedData.cs +++ b/sample/src/NimblePros.SampleToDo.Web/SeedData.cs @@ -6,33 +6,31 @@ namespace NimblePros.SampleToDo.Web; public static class SeedData { - public static readonly Contributor Contributor1 = new (ContributorName.From("Ardalis")); - public static readonly Contributor Contributor2 = new (ContributorName.From("Snowfrog")); + public static readonly Contributor Contributor1 = new(ContributorName.From("Ardalis")); + public static readonly Contributor Contributor2 = new(ContributorName.From("Snowfrog")); public static readonly Project TestProject1 = new Project(ProjectName.From("Test Project")); - public static readonly ToDoItem ToDoItem1 = new ToDoItem - { - Title = "Get Sample Working", - Description = "Try to get the sample to build." - }; - public static readonly ToDoItem ToDoItem2 = new ToDoItem - { - Title = "Review Solution", - Description = "Review the different projects in the solution and how they relate to one another." - }; - public static readonly ToDoItem ToDoItem3 = new ToDoItem - { - Title = "Run and Review Tests", - Description = "Make sure all the tests run and review what they are doing." - }; + + public static readonly ToDoItem ToDoItem1 = + new ToDoItem(title: ToDoItemTitle.From("Get Sample Working"), + description: ToDoItemDescription.From("Try to get the sample to build.")); + + public static readonly ToDoItem ToDoItem2 = + new ToDoItem(title: ToDoItemTitle.From("Review Solution"), + description: ToDoItemDescription.From("Review the different projects in the solution and how they relate to one another.")); + + + public static readonly ToDoItem ToDoItem3 = +new ToDoItem(title: ToDoItemTitle.From("Run and Review Tests"), + description: ToDoItemDescription.From("Make sure all the tests run and review what they are doing.")); public static async Task InitializeAsync(AppDbContext dbContext) { - if (await dbContext.ToDoItems.AnyAsync()) - { - return; // DB has been seeded - } + if (await dbContext.ToDoItems.AnyAsync()) + { + return; // DB has been seeded + } - await PopulateTestDataAsync(dbContext); + await PopulateTestDataAsync(dbContext); } public static async Task PopulateTestDataAsync(AppDbContext dbContext) diff --git a/sample/src/NimblePros.SampleToDo.Web/i18n/en/Project.json b/sample/src/NimblePros.SampleToDo.Web/i18n/en/Project.json new file mode 100644 index 000000000..ee15da8bc --- /dev/null +++ b/sample/src/NimblePros.SampleToDo.Web/i18n/en/Project.json @@ -0,0 +1,8 @@ +{ + "CoreProjectNameEmpty": "Name cannot be empty", + "CoreProjectNameTooLong": "Name cannot be longer than {0} characters", + "CoreToDoItemDescriptionEmpty": "Description cannot be empty", + "CoreToDoItemDescriptionTooLong": "Description cannot be longer than {0} characters", + "CoreToDoItemTitleEmpty": "Title cannot be empty", + "CoreToDoItemTitleTooLong": "Title cannot be longer than {0} characters" +} \ No newline at end of file diff --git a/sample/src/NimblePros.SampleToDo.Web/i18n/fa/Project.json b/sample/src/NimblePros.SampleToDo.Web/i18n/fa/Project.json new file mode 100644 index 000000000..ef3d8e37d --- /dev/null +++ b/sample/src/NimblePros.SampleToDo.Web/i18n/fa/Project.json @@ -0,0 +1,8 @@ +{ + "CoreProjectNameEmpty": "نام نمی‌تواند خالی باشد", + "CoreProjectNameTooLong": "نام نمی‌تواند بیش از {0} کاراکتر باشد", + "CoreToDoItemDescriptionEmpty": "توضیحات نمی‌تواند خالی باشد", + "CoreToDoItemDescriptionTooLong": "توضیحات نمی‌تواند بیش از {0} کاراکتر باشد", + "CoreToDoItemTitleEmpty": "عنوان نمی‌تواند خالی باشد", + "CoreToDoItemTitleTooLong": "عنوان نمی‌تواند بیش از {0} کاراکتر باشد" +} diff --git a/sample/src/NimblePros.SampleToDo.Web/i18n/fr/Project.json b/sample/src/NimblePros.SampleToDo.Web/i18n/fr/Project.json new file mode 100644 index 000000000..354d1ea08 --- /dev/null +++ b/sample/src/NimblePros.SampleToDo.Web/i18n/fr/Project.json @@ -0,0 +1,8 @@ +{ + "CoreProjectNameEmpty": "Le nom ne peut pas être vide", + "CoreProjectNameTooLong": "Le nom ne peut pas dépasser {0} caractères", + "CoreToDoItemDescriptionEmpty": "La description ne peut pas être vide", + "CoreToDoItemDescriptionTooLong": "La description ne peut pas dépasser {0} caractères", + "CoreToDoItemTitleEmpty": "Le titre ne peut pas être vide", + "CoreToDoItemTitleTooLong": "Le titre ne peut pas dépasser {0} caractères" +} diff --git a/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryAdd.cs b/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryAdd.cs index e39eddd32..13ca08b4c 100644 --- a/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryAdd.cs +++ b/sample/tests/NimblePros.SampleToDo.IntegrationTests/Data/EfRepositoryAdd.cs @@ -12,7 +12,7 @@ public async Task AddsProjectAndSetsId() var project = new Project(testProjectName); var item = new ToDoItem(); - item.Title = "test item title"; + item.UpdateTitle(ToDoItemTitle.From("test item title")); project.AddItem(item); await repository.AddAsync(project); diff --git a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/ProjectConstructor.cs b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/ProjectConstructor.cs index 278a7d394..2ea990ea7 100644 --- a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/ProjectConstructor.cs +++ b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/ProjectConstructor.cs @@ -1,4 +1,5 @@ -using NimblePros.SampleToDo.Core.ProjectAggregate; +using System.Globalization; +using NimblePros.SampleToDo.Core.ProjectAggregate; namespace NimblePros.SampleToDo.UnitTests.Core.ProjectAggregate; @@ -36,4 +37,16 @@ public void InitializesStatusToInProgress() Assert.Equal(ProjectStatus.Complete, _testProject.Status); } + [Fact] + public void ProjectName_TooLong_ReturnsLocalizedMessage() + { + // Arrange: Set a French culture for testing + Thread.CurrentThread.CurrentUICulture = new CultureInfo("en"); + + // Act + var message = ProjectErrorMessages.CoreProjectNameTooLong(100); + + // Assert: Should use fallback or loaded JSON (depending on setup) + Assert.Equal("Name cannot be longer than 100 characters", message); + } } diff --git a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/Project_AddItem.cs b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/Project_AddItem.cs index 8d62c40e3..e623aaf92 100644 --- a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/Project_AddItem.cs +++ b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/ProjectAggregate/Project_AddItem.cs @@ -9,11 +9,8 @@ public class Project_AddItem [Fact] public void AddsItemToItems() { - var _testItem = new ToDoItem - { - Title = "title", - Description = "description" - }; + var _testItem = new ToDoItem(title: ToDoItemTitle.From("title"), + description: ToDoItemDescription.From("description")); _testProject.AddItem(_testItem); diff --git a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Services/ToDoItemSearchServiceTests.cs b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Services/ToDoItemSearchServiceTests.cs index 08004d539..59bcc1e74 100644 --- a/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Services/ToDoItemSearchServiceTests.cs +++ b/sample/tests/NimblePros.SampleToDo.UnitTests/Core/Services/ToDoItemSearchServiceTests.cs @@ -1,7 +1,6 @@ using NimblePros.SampleToDo.Core.Interfaces; using NimblePros.SampleToDo.Core.ProjectAggregate; using NimblePros.SampleToDo.Core.Services; -using NimblePros.SharedKernel; namespace NimblePros.SampleToDo.UnitTests.Core.Services; @@ -9,21 +8,21 @@ public class ToDoItemSearchServiceTests { private readonly IToDoItemSearchService _service; private readonly IRepository _repo = Substitute.For>(); - + public ToDoItemSearchServiceTests() { _service = new ToDoItemSearchService(_repo); - + } [Fact] public async Task ReturnsValidationErrors() { var projects = await _service.GetAllIncompleteItemsAsync(ProjectId.From(0), string.Empty); - + Assert.NotEmpty(projects.ValidationErrors); } - + [Fact] public async Task ReturnsProjectNotFound() { @@ -31,18 +30,14 @@ public async Task ReturnsProjectNotFound() Assert.Equal(ResultStatus.NotFound, projects.Status); } - + [Fact] public async Task ReturnsAllIncompleteItems() { var title = "Some Title"; Project project = new Project(ProjectName.From("Cool Project")); - - project.AddItem(new ToDoItem - { - Title = title, - Description = "Some Description" - }); + + project.AddItem(new ToDoItem(title: ToDoItemTitle.From(title), description: ToDoItemDescription.From("Some Description"))); _repo.FirstOrDefaultAsync(Arg.Any>(), Arg.Any()) .Returns(project); @@ -50,7 +45,7 @@ public async Task ReturnsAllIncompleteItems() var projects = await _service.GetAllIncompleteItemsAsync(ProjectId.From(1), title); Assert.Empty(projects.ValidationErrors); - Assert.Equal(projects.Value.First().Title, title); + Assert.Equal(projects.Value.First().Title.Value, title); Assert.Equal(project.Items.Count(), projects.Value.Count); } } diff --git a/sample/tests/NimblePros.SampleToDo.UnitTests/ToDoItemBuilder.cs b/sample/tests/NimblePros.SampleToDo.UnitTests/ToDoItemBuilder.cs index 423d33cdc..08f851e14 100644 --- a/sample/tests/NimblePros.SampleToDo.UnitTests/ToDoItemBuilder.cs +++ b/sample/tests/NimblePros.SampleToDo.UnitTests/ToDoItemBuilder.cs @@ -14,21 +14,21 @@ public ToDoItemBuilder Id(int id) return this; } - public ToDoItemBuilder Title(string title) + public ToDoItemBuilder Title(String title) { - _todo.Title = title; + _todo.UpdateTitle(ToDoItemTitle.From(title)); return this; } - public ToDoItemBuilder Description(string description) + public ToDoItemBuilder Description(String description) { - _todo.Description = description; + _todo.UpdateDescription(ToDoItemDescription.From(description)); return this; } public ToDoItemBuilder WithDefaultValues() { - _todo = new ToDoItem() { Id = ToDoItemId.From(1), Title = "Test Item", Description = "Test Description" }; + _todo = new ToDoItem(title: ToDoItemTitle.From("Test Item"), description: ToDoItemDescription.From("Test Description")) { Id = ToDoItemId.From(1) }; return this; }