From 9b3955e94d3f5ff3e2b345674c0931733fb4b7d7 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:06:56 +0200 Subject: [PATCH 01/29] Fix encoded email subjects --- src/Sections/Humans.Email/Docs/debt.yml | 6 +---- .../Services/GoogleIntegrationEmails.cs | 6 ++--- .../Humans.Issues/Services/IssuesEmails.cs | 2 +- .../Humans.Shifts/Services/ShiftsEmails.cs | 4 +-- .../GoogleIntegrationEmailsTests.cs | 25 +++++++++++++++++++ .../TestGoogleIntegrationEmails.cs | 5 ++-- .../Services/IssuesEmailsTests.cs | 9 +++++++ .../Infrastructure/TestShiftsEmails.cs | 5 ++-- .../Services/ShiftsEmailsTests.cs | 21 ++++++++++++++++ 9 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/Sections/Humans.Email/Docs/debt.yml b/src/Sections/Humans.Email/Docs/debt.yml index f5d60efc27..a911a63056 100644 --- a/src/Sections/Humans.Email/Docs/debt.yml +++ b/src/Sections/Humans.Email/Docs/debt.yml @@ -2,8 +2,4 @@ # /debt-sweep pools every src/Sections/*/Docs/debt.yml into the same inbox as the central ledger. version: 1 next_id: 3 -inbox: - - added: 2026-09-11 - id: EMAIL-2 - what: "src/Sections/Humans.Email/Services/EmailRenderer.cs HTML-encodes these subject lines: Email_IssueComment_Subject, Email_CoordinatorRotaMessage_Subject, Email_CoordinatorTeamRotasMessage_Subject, Email_GoogleGroupRemoval_LossOfAccess_Subject, Email_GoogleDriveRemoval_LossOfAccess_Subject, Email_GoogleAccessRemoval_SecondaryCleanup_Subject. A Subject header is plain text, so a title containing & or < reaches the recipient as R&D. The assembly-vote subjects had the same bug and were fixed in peterdrier/Humans#1649; these are left alone as out of that PR's scope." - review: light +inbox: [] diff --git a/src/Sections/Humans.GoogleIntegration/Services/GoogleIntegrationEmails.cs b/src/Sections/Humans.GoogleIntegration/Services/GoogleIntegrationEmails.cs index 18819d9c9e..ed6f4895ec 100644 --- a/src/Sections/Humans.GoogleIntegration/Services/GoogleIntegrationEmails.cs +++ b/src/Sections/Humans.GoogleIntegration/Services/GoogleIntegrationEmails.cs @@ -35,7 +35,7 @@ public EmailMessage WorkspaceCredentials(string recoveryEmail, string userName, public EmailMessage GoogleGroupRemovalLossOfAccess(string removedEmail, string userName, string groupName, string groupEmail, string? culture = null) => Localized(culture, () => new EmailMessage( removedEmail, userName, - Lf("GoogleIntegration_Email_GoogleGroupRemoval_LossOfAccess_Subject", Encode(groupEmail)), + Lf("GoogleIntegration_Email_GoogleGroupRemoval_LossOfAccess_Subject", groupEmail), Lf("GoogleIntegration_Email_GoogleGroupRemoval_LossOfAccess_Body", Encode(userName), Encode(groupName), Encode(groupEmail)), "google_group_removal_loss_of_access", MessageCategory.System)); @@ -43,7 +43,7 @@ public EmailMessage GoogleGroupRemovalLossOfAccess(string removedEmail, string u public EmailMessage GoogleDriveRemovalLossOfAccess(string removedEmail, string userName, string folderName, string? culture = null) => Localized(culture, () => new EmailMessage( removedEmail, userName, - Lf("GoogleIntegration_Email_GoogleDriveRemoval_LossOfAccess_Subject", Encode(folderName)), + Lf("GoogleIntegration_Email_GoogleDriveRemoval_LossOfAccess_Subject", folderName), Lf("GoogleIntegration_Email_GoogleDriveRemoval_LossOfAccess_Body", Encode(userName), Encode(folderName)), "google_drive_removal_loss_of_access", MessageCategory.System)); @@ -51,7 +51,7 @@ public EmailMessage GoogleDriveRemovalLossOfAccess(string removedEmail, string u public EmailMessage GoogleAccessRemovalSecondaryCleanup(string removedEmail, string userName, string currentGoogleEmail, string? culture = null) => Localized(culture, () => new EmailMessage( removedEmail, userName, - Lf("GoogleIntegration_Email_GoogleAccessRemoval_SecondaryCleanup_Subject", Encode(removedEmail)), + Lf("GoogleIntegration_Email_GoogleAccessRemoval_SecondaryCleanup_Subject", removedEmail), Lf("GoogleIntegration_Email_GoogleAccessRemoval_SecondaryCleanup_Body", Encode(userName), Encode(removedEmail), Encode(currentGoogleEmail)), "google_access_removal_secondary_cleanup", MessageCategory.System)); diff --git a/src/Sections/Humans.Issues/Services/IssuesEmails.cs b/src/Sections/Humans.Issues/Services/IssuesEmails.cs index c787cdf207..9c28416229 100644 --- a/src/Sections/Humans.Issues/Services/IssuesEmails.cs +++ b/src/Sections/Humans.Issues/Services/IssuesEmails.cs @@ -28,7 +28,7 @@ internal sealed class IssuesEmails( public EmailMessage IssueComment(string to, string displayName, string issueTitle, string commentContent, string issueLink, string preferredLanguage) => Localized(preferredLanguage, () => new EmailMessage( to, displayName, - Lf("Issues_Email_IssueComment_Subject", Encode(issueTitle)), + Lf("Issues_Email_IssueComment_Subject", issueTitle), Lf("Issues_Email_IssueComment_Body", Encode(displayName), Encode(issueTitle), SanitizedMarkdownRenderer.Render(commentContent), Encode(AbsoluteUrl(issueLink))), "issue_comment", MessageCategory.System)); diff --git a/src/Sections/Humans.Shifts/Services/ShiftsEmails.cs b/src/Sections/Humans.Shifts/Services/ShiftsEmails.cs index 9b3196ffd3..9ec189aced 100644 --- a/src/Sections/Humans.Shifts/Services/ShiftsEmails.cs +++ b/src/Sections/Humans.Shifts/Services/ShiftsEmails.cs @@ -33,7 +33,7 @@ public EmailMessage CoordinatorRotaMessage(CoordinatorRotaMessageRequest request : ""; return new EmailMessage(request.RecipientEmail, request.RecipientName, - Lf("Shifts_Email_CoordinatorRotaMessage_Subject", Encode(request.RotaName)), + Lf("Shifts_Email_CoordinatorRotaMessage_Subject", request.RotaName), Lf("Shifts_Email_CoordinatorRotaMessage_Body", Encode(request.RecipientName), Encode(request.SenderName), @@ -73,7 +73,7 @@ public EmailMessage CoordinatorTeamRotasMessage(CoordinatorTeamRotasMessageReque } return new EmailMessage(request.RecipientEmail, request.RecipientName, - Lf("Shifts_Email_CoordinatorTeamRotasMessage_Subject", Encode(request.TeamName)), + Lf("Shifts_Email_CoordinatorTeamRotasMessage_Subject", request.TeamName), Lf("Shifts_Email_CoordinatorTeamRotasMessage_Body", Encode(request.RecipientName), Encode(request.SenderName), diff --git a/tests/Humans.GoogleIntegration.Tests/GoogleIntegrationEmailsTests.cs b/tests/Humans.GoogleIntegration.Tests/GoogleIntegrationEmailsTests.cs index 80f84fa79d..17bbb4d37a 100644 --- a/tests/Humans.GoogleIntegration.Tests/GoogleIntegrationEmailsTests.cs +++ b/tests/Humans.GoogleIntegration.Tests/GoogleIntegrationEmailsTests.cs @@ -61,6 +61,31 @@ public void RendersInTheRecipientsCulture_AndEncodesTheResourceName() msg.HtmlBody.Should().Contain("Lights & sound"); } + [HumansFact] + public void RemovalNotices_keep_html_encoding_out_of_plain_text_subjects() + { + var emails = TestGoogleIntegrationEmails.Create(new Dictionary(StringComparer.Ordinal) + { + ["GoogleIntegration_Email_GoogleGroupRemoval_LossOfAccess_Subject"] = "Removed from {0}", + ["GoogleIntegration_Email_GoogleDriveRemoval_LossOfAccess_Subject"] = "Access to {0} removed", + ["GoogleIntegration_Email_GoogleAccessRemoval_SecondaryCleanup_Subject"] = "{0} lost access", + ["GoogleIntegration_Email_GoogleGroupRemoval_LossOfAccess_Body"] = "

{0}

{1}

{2}

", + ["GoogleIntegration_Email_GoogleDriveRemoval_LossOfAccess_Body"] = "

{0}

{1}

", + ["GoogleIntegration_Email_GoogleAccessRemoval_SecondaryCleanup_Body"] = "

{0}

{1}

{2}

" + }); + + var group = emails.GoogleGroupRemovalLossOfAccess("a@x.com", "Alice", "Art", "art & design@nobodies.team", "en"); + var drive = emails.GoogleDriveRemovalLossOfAccess("a@x.com", "Alice", "Lights & sound", "en"); + var cleanup = emails.GoogleAccessRemovalSecondaryCleanup("old & new@x.com", "Alice", "new@x.com", "en"); + + group.Subject.Should().Be("Removed from art & design@nobodies.team"); + group.HtmlBody.Should().Contain("art & design@nobodies.team"); + drive.Subject.Should().Be("Access to Lights & sound removed"); + drive.HtmlBody.Should().Contain("Lights & sound"); + cleanup.Subject.Should().Be("old & new@x.com lost access"); + cleanup.HtmlBody.Should().Contain("old & new@x.com"); + } + [HumansFact] public void EveryTemplateHasAPreviewSample() { diff --git a/tests/Humans.GoogleIntegration.Tests/Infrastructure/TestGoogleIntegrationEmails.cs b/tests/Humans.GoogleIntegration.Tests/Infrastructure/TestGoogleIntegrationEmails.cs index 782f8b20db..21e336a3e6 100644 --- a/tests/Humans.GoogleIntegration.Tests/Infrastructure/TestGoogleIntegrationEmails.cs +++ b/tests/Humans.GoogleIntegration.Tests/Infrastructure/TestGoogleIntegrationEmails.cs @@ -23,14 +23,15 @@ internal static class TestGoogleIntegrationEmails ["GoogleIntegration_Email_GoogleAccessRemoval_SecondaryCleanup_Body"] = "

{0}

{1}

{2}

", }; - public static GoogleIntegrationEmails Create() + public static GoogleIntegrationEmails Create(IReadOnlyDictionary? formats = null) { + formats ??= Formats; var localizer = Substitute.For>(); localizer[Arg.Any()].Returns(call => { var key = call.Arg(); return new LocalizedString(key, - Formats.GetValueOrDefault(key, $"{key}#{CultureInfo.CurrentUICulture.Name}")); + formats.GetValueOrDefault(key, $"{key}#{CultureInfo.CurrentUICulture.Name}")); }); return new GoogleIntegrationEmails(localizer, NullLogger.Instance); diff --git a/tests/Humans.Issues.Tests/Services/IssuesEmailsTests.cs b/tests/Humans.Issues.Tests/Services/IssuesEmailsTests.cs index 6b71196a12..cdeaf09084 100644 --- a/tests/Humans.Issues.Tests/Services/IssuesEmailsTests.cs +++ b/tests/Humans.Issues.Tests/Services/IssuesEmailsTests.cs @@ -50,6 +50,15 @@ public void IssueComment_renders_sanitized_markdown_with_https_images() msg.HtmlBody.Should().Contain("? formats = null) { + formats ??= Formats; var localizer = Substitute.For>(); localizer[Arg.Any()].Returns(call => { var key = call.Arg(); return new LocalizedString(key, - Formats.GetValueOrDefault(key, $"{key}#{CultureInfo.CurrentUICulture.Name}")); + formats.GetValueOrDefault(key, $"{key}#{CultureInfo.CurrentUICulture.Name}")); }); return new ShiftsEmails(localizer, NullLogger.Instance); diff --git a/tests/Humans.Shifts.Tests/Services/ShiftsEmailsTests.cs b/tests/Humans.Shifts.Tests/Services/ShiftsEmailsTests.cs index e42989ea38..4a9861d99d 100644 --- a/tests/Humans.Shifts.Tests/Services/ShiftsEmailsTests.cs +++ b/tests/Humans.Shifts.Tests/Services/ShiftsEmailsTests.cs @@ -89,6 +89,27 @@ public void CoordinatorTeamRotasMessage_renders_markdown_instead_of_html_encoded msg.HtmlBody.Should().NotContain("(StringComparer.Ordinal) + { + ["Shifts_Email_CoordinatorRotaMessage_Subject"] = "Message about {0}", + ["Shifts_Email_CoordinatorTeamRotasMessage_Subject"] = "Message with {0}", + ["Shifts_Email_CoordinatorRotaMessage_Body"] = "

Dear {0},

From {1} on {2}:

{3}{4}{5}", + ["Shifts_Email_CoordinatorTeamRotasMessage_Body"] = "

Dear {0},

From {1} on {2}:

{3}{4}{5}" + }); + var rota = emails.CoordinatorRotaMessage(RotaRequest() with { RotaName = "Lights & sound" }); + var team = emails.CoordinatorTeamRotasMessage(new CoordinatorTeamRotasMessageRequest( + "rcpt@x.com", "Recipient", "Sender", "coord@x.com", "Bar & kitchen", "Hello", + [new CoordinatorRotaShiftGroup("Gate", ["Mon"])], "en")); + + rota.Subject.Should().Be("Message about Lights & sound"); + rota.HtmlBody.Should().Contain("Lights & sound"); + team.Subject.Should().Be("Message with Bar & kitchen"); + team.HtmlBody.Should().Contain("Bar & kitchen"); + } + [HumansFact] public void EveryTemplateHasAPreviewSample() { From 37f0d5b4920b29bf57e30fbb3c08e49f58fc2e40 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:08:01 +0200 Subject: [PATCH 02/29] Deduplicate ticket cache resolver --- src/Sections/Humans.Tickets/Docs/debt.yml | 4 ---- .../Services/Stores/CachingTicketQueryService.cs | 15 +++++---------- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/src/Sections/Humans.Tickets/Docs/debt.yml b/src/Sections/Humans.Tickets/Docs/debt.yml index 3e65347a65..0ad7c3a0d6 100644 --- a/src/Sections/Humans.Tickets/Docs/debt.yml +++ b/src/Sections/Humans.Tickets/Docs/debt.yml @@ -11,10 +11,6 @@ inbox: id: TICKETS-2 what: "TicketController.Orders/Attendees/Codes/SalesAggregates copy service DTOs into Models/TicketViewModels.cs field-by-field (TicketDashboardDtos.cs mirrored, ~150 lines; the Attendees VIP split IsVip/TaxableAmount/VipDonation is computed in the controller). Collapse: bind the DTOs directly as TicketTransferAdmin/Detail and Onsite already do, moving the VIP split onto the AttendeeRow DTO or into the view; the views declare the row types in Func helpers, so each needs a render check. Skipped by the 2026-09-05 run (finding 6) because the cloud container has no Postgres for that check." review: panel - - added: 2026-09-05 - id: TICKETS-3 - what: "CachingTicketQueryService carries WithInner twice — once on the outer class and once on the nested UserHoldingsCache. One resolver would do; touch when the file is next open for a behavior change (finding S4, /section-doctor on Tickets 2026-09-05)." - review: light - added: 2026-09-11 id: TICKETS-4 what: "TicketTransferService.ProcessTransferAsync / ApproveAsync / RejectAsync guard on request.Status == Pending with a plain read-then-write, so two overlapping Decide POSTs on the same request both pass the check and can both enter the vendor void/reissue flow (the Decide form has no submit guard either). no-concurrency-tokens rules out a row version; the fix is an idempotent guard in the service (e.g. a Processing status written before the vendor call) or a disabled-on-submit form. Raised by Codex on peterdrier/Humans#1589; health.md records it as a gap." diff --git a/src/Sections/Humans.Tickets/Services/Stores/CachingTicketQueryService.cs b/src/Sections/Humans.Tickets/Services/Stores/CachingTicketQueryService.cs index 53f624bde1..1b6c631868 100644 --- a/src/Sections/Humans.Tickets/Services/Stores/CachingTicketQueryService.cs +++ b/src/Sections/Humans.Tickets/Services/Stores/CachingTicketQueryService.cs @@ -31,7 +31,9 @@ public CachingTicketQueryService( _orders = new OrdersCache( async ct => await WithInner(inner => inner.GetTicketOrdersAsync(ct)), logger); - _userHoldings = new UserHoldingsCache(scopeFactory, clock, UserHoldingsCacheTtl, logger); + _userHoldings = new UserHoldingsCache( + (userId, ct) => WithInner(inner => inner.GetUserTicketHoldingsAsync(userId, ct)), + clock, UserHoldingsCacheTtl, logger); } public ICacheStats OrdersCacheStats => _orders; @@ -160,7 +162,7 @@ protected override async Task WarmAllAsync(CancellationToken ct) } private sealed class UserHoldingsCache( - IServiceScopeFactory scopeFactory, + Func> loadHoldings, IClock clock, Duration ttl, ILogger logger) @@ -183,16 +185,9 @@ private sealed class UserHoldingsCache( protected override async ValueTask LoadRowAsync(Guid userId, CancellationToken ct) { - var holdings = await WithInner(inner => inner.GetUserTicketHoldingsAsync(userId, ct)); + var holdings = await loadHoldings(userId, ct); return new CachedUserTicketHoldings(holdings, clock.GetCurrentInstant() + ttl); } - - private async Task WithInner(Func> action) - { - await using var scope = scopeFactory.CreateAsyncScope(); - var inner = scope.ServiceProvider.GetRequiredKeyedService(InnerServiceKey); - return await action(inner); - } } private sealed record CachedUserTicketHoldings(UserTicketHoldings Value, Instant ExpiresAt); From 124a89f99636544f91688483d654964226cda59d Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:09:27 +0200 Subject: [PATCH 03/29] Remove unused MailerLite date converter --- src/Sections/Humans.MailerLite/Docs/debt.yml | 6 +-- .../Services/MailerLite/MailerLiteClient.cs | 1 - .../MailerLite/MailerLiteDateConverter.cs | 33 --------------- .../Services/MailerLiteDateConverterTests.cs | 40 ------------------- 4 files changed, 1 insertion(+), 79 deletions(-) delete mode 100644 src/Sections/Humans.MailerLite/Services/MailerLite/MailerLiteDateConverter.cs delete mode 100644 tests/Humans.MailerLite.Tests/Services/MailerLiteDateConverterTests.cs diff --git a/src/Sections/Humans.MailerLite/Docs/debt.yml b/src/Sections/Humans.MailerLite/Docs/debt.yml index 1df95fe861..6e721c78b8 100644 --- a/src/Sections/Humans.MailerLite/Docs/debt.yml +++ b/src/Sections/Humans.MailerLite/Docs/debt.yml @@ -2,8 +2,4 @@ # /debt-sweep pools every src/Sections/*/Docs/debt.yml into the same inbox as the central ledger. version: 1 next_id: 2 -inbox: - - added: 2026-08-25 - id: MLITE-1 - what: "Decide whether src/Sections/Humans.MailerLite/Services/MailerLite/MailerLiteDateConverter.cs stays. It is still registered on the client's JSON options (MailerLiteClient.cs line 354) and has its own test file, but no DTO property it could convert survives: MailerLiteGroup.CreatedAt was the last Instant? on the wire and was deleted by /section-doctor on MailerLite 2026-08-25. Either a nullable Instant? comes back onto a DTO or the converter, its registration and its tests go." - review: light +inbox: [] diff --git a/src/Sections/Humans.MailerLite/Services/MailerLite/MailerLiteClient.cs b/src/Sections/Humans.MailerLite/Services/MailerLite/MailerLiteClient.cs index cdde7c5aec..0ed916e334 100644 --- a/src/Sections/Humans.MailerLite/Services/MailerLite/MailerLiteClient.cs +++ b/src/Sections/Humans.MailerLite/Services/MailerLite/MailerLiteClient.cs @@ -351,7 +351,6 @@ private static JsonSerializerOptions BuildJson() PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNameCaseInsensitive = true, }; - o.Converters.Add(new MailerLiteDateConverter()); o.Converters.Add(new MailerLiteSubscriberConverter()); return o; } diff --git a/src/Sections/Humans.MailerLite/Services/MailerLite/MailerLiteDateConverter.cs b/src/Sections/Humans.MailerLite/Services/MailerLite/MailerLiteDateConverter.cs deleted file mode 100644 index 2ba6cbb796..0000000000 --- a/src/Sections/Humans.MailerLite/Services/MailerLite/MailerLiteDateConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Globalization; -using System.Text.Json; -using System.Text.Json.Serialization; -using Humans.Base.Extensions; -using NodaTime; - -namespace Humans.MailerLite.Services.MailerLite; - -/// -/// JSON converter for MailerLite timestamp fields. Format is -/// "YYYY-MM-DD HH:MM:SS" (space separator, no offset). Treated as UTC -/// per ML docs. -/// -internal sealed class MailerLiteDateConverter : JsonConverter -{ - private const string Format = "yyyy-MM-dd HH:mm:ss"; - - public override Instant? Read(ref Utf8JsonReader reader, Type _, JsonSerializerOptions __) - { - if (reader.TokenType == JsonTokenType.Null) return null; - var raw = reader.GetString(); - if (string.IsNullOrEmpty(raw)) return null; - var dt = DateTime.ParseExact(raw, Format, CultureInfo.InvariantCulture, - DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); - return Instant.FromDateTimeUtc(DateTime.SpecifyKind(dt, DateTimeKind.Utc)); - } - - public override void Write(Utf8JsonWriter writer, Instant? value, JsonSerializerOptions _) - { - if (value is null) writer.WriteNullValue(); - else writer.WriteStringValue(value.Value.ToDateTimeUtc().ToInvariantTimestamp()); - } -} diff --git a/tests/Humans.MailerLite.Tests/Services/MailerLiteDateConverterTests.cs b/tests/Humans.MailerLite.Tests/Services/MailerLiteDateConverterTests.cs deleted file mode 100644 index e3131f7838..0000000000 --- a/tests/Humans.MailerLite.Tests/Services/MailerLiteDateConverterTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Text.Json; -using AwesomeAssertions; -using Humans.MailerLite.Services.MailerLite; -using NodaTime; - -namespace Humans.MailerLite.Tests.Services; - -public class MailerLiteDateConverterTests -{ - private static readonly JsonSerializerOptions Opts = new() - { - Converters = { new MailerLiteDateConverter() } - }; - - private sealed record Wrap(Instant? At); - - [HumansFact] - public void Reads_MailerLiteSpaceSeparatedDateAsUtc() - { - var json = """{"At":"2026-05-12 09:30:00"}"""; - var wrap = JsonSerializer.Deserialize(json, Opts); - wrap!.At.Should().Be(Instant.FromUtc(2026, 5, 12, 9, 30, 0)); - } - - [HumansFact] - public void Reads_NullAsNull() - { - var json = """{"At":null}"""; - var wrap = JsonSerializer.Deserialize(json, Opts); - wrap!.At.Should().BeNull(); - } - - [HumansFact] - public void Reads_MissingFieldAsNull() - { - var json = "{}"; - var wrap = JsonSerializer.Deserialize(json, Opts); - wrap!.At.Should().BeNull(); - } -} From 2f7bd07691ab789ee55a5548ad0da2e749e707c1 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:12:01 +0200 Subject: [PATCH 04/29] Move camp role slot sorting out of repository --- src/Sections/Humans.Camps/Data/CampRepository.Roles.cs | 1 - .../Baselines/DisplaySortInControllers.baseline.txt | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs b/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs index 2759ac4ad2..082449ef4e 100644 --- a/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs +++ b/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs @@ -135,7 +135,6 @@ public async Task> GetAssignmentsForSeasonAsyn .Include(a => a.Definition) .Include(a => a.CampMember) .Where(a => a.CampSeasonId == campSeasonId) - .OrderBy(a => a.Definition.SortOrder).ThenBy(a => a.AssignedAt) .ToListAsync(ct); } diff --git a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt index 58f65b6872..99232c8336 100644 --- a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt +++ b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt @@ -8,8 +8,6 @@ src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:OrderBy#1 src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:ThenBy#1 -src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:OrderBy#2 -src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:ThenBy#2 src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs:OrderBy#1 src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs:OrderBy#2 src/Sections/Humans.Teams/Data/TeamRepository.cs:OrderBy#1 From 246012750a8b2e12896fbc98fbf4f08c86fcc975 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:15:01 +0200 Subject: [PATCH 05/29] Consolidate shift signup helpers --- src/Sections/Humans.Shifts/Docs/debt.yml | 8 ----- .../Services/ShiftSignupService.cs | 33 +++++++++++-------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/Sections/Humans.Shifts/Docs/debt.yml b/src/Sections/Humans.Shifts/Docs/debt.yml index 22811cf393..ca2828b824 100644 --- a/src/Sections/Humans.Shifts/Docs/debt.yml +++ b/src/Sections/Humans.Shifts/Docs/debt.yml @@ -3,14 +3,6 @@ version: 1 next_id: 5 inbox: - - added: 2026-09-05 - id: SHIFTS-1 - what: "ShiftSignupService re-derives 'is privileged' as `flags.HasFlag(Privileged) || await shiftMgmt.CanApproveSignupsAsync(...)` at several sites (lines 61, 88, 165, 223, 544, 603, 848, 967 as of 2026-09-20). One private helper. Found by /section-doctor on Shifts 2026-09-05 (finding 18). (split 2026-09-20 from the bundled row of the same date; one row per independently-fixable defect.)" - review: light - - added: 2026-09-05 - id: SHIFTS-2 - what: "ShiftSignupService formats the audit day label `es.GateOpeningDate.PlusDays(dayOffset).ToWeekdayDayMonth()` inline at several sites (lines 119, 306, 433, 757, 809 as of 2026-09-20). One private helper. Found by /section-doctor on Shifts 2026-09-05 (finding 18). (split 2026-09-20 from the bundled row of the same date; one row per independently-fixable defect.)" - review: light - added: 2026-09-05 id: SHIFTS-3 what: "The GateOpeningDate.PlusDays(offset) window math is written out at ShiftManagementService, ShiftFilterResolver.cs:32-42, VolunteerTrackingController.cs:140-154, ShiftDashboardPageBuilder — where one helper on the event calendar would carry it (found by /section-doctor on Shifts, 2026-09-05, finding N12)." diff --git a/src/Sections/Humans.Shifts/Services/ShiftSignupService.cs b/src/Sections/Humans.Shifts/Services/ShiftSignupService.cs index 10094ea7f9..998cf9d58e 100644 --- a/src/Sections/Humans.Shifts/Services/ShiftSignupService.cs +++ b/src/Sections/Humans.Shifts/Services/ShiftSignupService.cs @@ -67,7 +67,7 @@ public async Task SignUpAsync( var calendar = await calendarResolver.GetAsync(shift.Rota.EventSettingsId); if (calendar is null) return SignupResult.Fail("Event calendar not configured."); var now = clock.GetCurrentInstant(); - isPrivileged = isPrivileged || await shiftMgmt.CanApproveSignupsAsync(userId, shift.Rota.TeamId); + isPrivileged = await IsPrivilegedAsync(userId, shift.Rota.TeamId, isPrivileged); if (!localEs.IsShiftBrowsingOpen && !isPrivileged) return SignupResult.Fail("Shift browsing is not currently open."); @@ -125,7 +125,7 @@ public async Task SignUpAsync( if (autoConfirm && shift.IsEarlyEntry) earlyEntryInvalidator.InvalidateUser(userId); - var shiftDate = calendar.GateOpeningDate.PlusDays(shift.DayOffset).ToWeekdayDayMonth(); + var shiftDate = FormatAuditDay(calendar, shift.DayOffset); var statusSuffix = autoConfirm ? "confirmed" : "pending"; await auditLogService.LogAsync( AuditAction.ShiftSignupCreated, nameof(ShiftSignup), signup.Id, @@ -172,7 +172,7 @@ public async Task ApproveAsync(Guid signupId, Guid reviewerUserId) var now = clock.GetCurrentInstant(); if (signup.Shift.IsEarlyEntry && calendar.IsEarlyEntryClosed(now)) { - var isPrivileged = await shiftMgmt.CanApproveSignupsAsync(reviewerUserId, signup.Shift.Rota.TeamId); + var isPrivileged = await IsPrivilegedAsync(reviewerUserId, signup.Shift.Rota.TeamId); if (!isPrivileged) return SignupResult.Fail("Cannot approve build shift signups after early entry close."); } @@ -231,7 +231,7 @@ public async Task BailAsync(Guid signupId, Guid actorUserId, strin if (calendar is null) return SignupResult.Fail("Event calendar not configured."); var now = clock.GetCurrentInstant(); var isOwner = signup.UserId == actorUserId; - var isPrivileged = await shiftMgmt.CanApproveSignupsAsync(actorUserId, signup.Shift.Rota.TeamId); + var isPrivileged = await IsPrivilegedAsync(actorUserId, signup.Shift.Rota.TeamId); // Auth: must be signup owner or privileged (dept coordinator/NoInfoAdmin/Admin) if (!isOwner && !isPrivileged) @@ -315,7 +315,7 @@ public async Task VoluntellAsync(Guid userId, Guid shiftId, Guid e await auditLogService.LogAsync( AuditAction.ShiftSignupVoluntold, nameof(ShiftSignup), signup.Id, - $"shift '{shift.Rota.Name}' on {calendar.GateOpeningDate.PlusDays(shift.DayOffset).ToWeekdayDayMonth()}", + $"shift '{shift.Rota.Name}' on {FormatAuditDay(calendar, shift.DayOffset)}", enrollerUserId, userId, nameof(User)); @@ -443,7 +443,7 @@ public async Task VoluntellRangeAsync(Guid userId, Guid rotaId, in { await auditLogService.LogAsync( AuditAction.ShiftSignupVoluntold, nameof(ShiftSignup), auditedSignup.Id, - $"'{rota.Name}' on {calendar.GateOpeningDate.PlusDays(dayOffset).ToWeekdayDayMonth()} (range)", + $"'{rota.Name}' on {FormatAuditDay(calendar, dayOffset)} (range)", enrollerUserId, userId, nameof(User)); } @@ -557,7 +557,7 @@ public async Task SignUpRangeAsync( var calendar = await calendarResolver.GetAsync(rota.EventSettingsId); if (calendar is null) return SignupResult.Fail("Event calendar not configured."); var now = clock.GetCurrentInstant(); - isPrivileged = isPrivileged || await shiftMgmt.CanApproveSignupsAsync(userId, rota.TeamId); + isPrivileged = await IsPrivilegedAsync(userId, rota.TeamId, isPrivileged); if (!localEs.IsShiftBrowsingOpen && !isPrivileged) return SignupResult.Fail("Shift browsing is not currently open."); @@ -771,8 +771,15 @@ private static string AppendRangeWarning(string? warning, string nextWarning) => warning is null ? nextWarning : $"{warning} {nextWarning}"; private static string FormatRangeDayList(EventSettingsInfo eventSettings, IEnumerable dayOffsets) - => string.Join(", ", dayOffsets.Select(offset => - eventSettings.GateOpeningDate.PlusDays(offset).ToWeekdayDayMonth())); + => string.Join(", ", dayOffsets.Select(offset => FormatAuditDay(eventSettings, offset))); + + private static string FormatAuditDay(EventSettingsInfo eventSettings, int dayOffset) => + eventSettings.GateOpeningDate.PlusDays(dayOffset).ToWeekdayDayMonth(); + + private Task IsPrivilegedAsync(Guid userId, Guid teamId, bool alreadyPrivileged = false) => + alreadyPrivileged + ? Task.FromResult(true) + : shiftMgmt.CanApproveSignupsAsync(userId, teamId); private RangeSignupCreation StageRangeSignups( Guid userId, @@ -824,7 +831,7 @@ private async Task AuditRangeSignupsAsync( await auditLogService.LogAsync( AuditAction.ShiftSignupCreated, nameof(ShiftSignup), auditedSignup.Id, - $"'{rota.Name}' on {eventSettings.GateOpeningDate.PlusDays(dayOffset).ToWeekdayDayMonth()} (range, {statusSuffix})", + $"'{rota.Name}' on {FormatAuditDay(eventSettings, dayOffset)} (range, {statusSuffix})", userId, userId, nameof(User)); } @@ -863,7 +870,7 @@ public async Task ApproveRangeAsync(Guid signupBlockId, Guid revie if (signup.Shift.IsEarlyEntry && calendar.IsEarlyEntryClosed(now)) { - var isPrivileged = await shiftMgmt.CanApproveSignupsAsync(reviewerUserId, signup.Shift.Rota.TeamId); + var isPrivileged = await IsPrivilegedAsync(reviewerUserId, signup.Shift.Rota.TeamId); if (!isPrivileged) return SignupResult.Fail("Cannot approve build shift signups after early entry close."); } @@ -983,7 +990,7 @@ public async Task BailRangeAsync(Guid signupBlockId, Guid actorUserId, string? r ?? throw new InvalidOperationException("Event calendar not configured."); var now = clock.GetCurrentInstant(); var isOwner = firstSignup.UserId == actorUserId; - var isPrivileged = await shiftMgmt.CanApproveSignupsAsync(actorUserId, firstSignup.Shift.Rota.TeamId); + var isPrivileged = await IsPrivilegedAsync(actorUserId, firstSignup.Shift.Rota.TeamId); if (!isOwner && !isPrivileged) throw new InvalidOperationException("Not authorized to bail this signup block."); @@ -1186,7 +1193,7 @@ private async Task DispatchSignupChangeNotificationAsync(ShiftSignup signup, Shi var calendar = await calendarResolver.GetAsync(rota.EventSettingsId); if (calendar is null) return; var shiftDate = calendar.GateOpeningDate.PlusDays(shift.DayOffset); - var enrichedDescription = $"{changeDescription} ({rotaName}, {shiftDate.ToWeekdayDayMonth()})"; + var enrichedDescription = $"{changeDescription} ({rotaName}, {FormatAuditDay(calendar, shift.DayOffset)})"; var team = await TeamService.GetTeamAsync(teamId); var coordinatorIds = team?.Members From d10c539147335546e08b9f3e008e832515fdd4f4 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:16:22 +0200 Subject: [PATCH 06/29] Remove unused shift date local --- src/Sections/Humans.Shifts/Services/ShiftSignupService.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Sections/Humans.Shifts/Services/ShiftSignupService.cs b/src/Sections/Humans.Shifts/Services/ShiftSignupService.cs index 998cf9d58e..8b091be5ca 100644 --- a/src/Sections/Humans.Shifts/Services/ShiftSignupService.cs +++ b/src/Sections/Humans.Shifts/Services/ShiftSignupService.cs @@ -1192,7 +1192,6 @@ private async Task DispatchSignupChangeNotificationAsync(ShiftSignup signup, Shi // Calendar comes from Settings, not this row's own (dead) columns (nobodies-collective/Humans#1631). var calendar = await calendarResolver.GetAsync(rota.EventSettingsId); if (calendar is null) return; - var shiftDate = calendar.GateOpeningDate.PlusDays(shift.DayOffset); var enrichedDescription = $"{changeDescription} ({rotaName}, {FormatAuditDay(calendar, shift.DayOffset)})"; var team = await TeamService.GetTeamAsync(teamId); From c21664c3edf729fcf3727c43a1500272d0e981a7 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:22:27 +0200 Subject: [PATCH 07/29] Localize container validation errors --- src/Sections/Humans.Containers/ContainersResource.ca.resx | 6 ++++++ src/Sections/Humans.Containers/ContainersResource.de.resx | 6 ++++++ src/Sections/Humans.Containers/ContainersResource.es.resx | 6 ++++++ src/Sections/Humans.Containers/ContainersResource.fr.resx | 6 ++++++ src/Sections/Humans.Containers/ContainersResource.it.resx | 6 ++++++ src/Sections/Humans.Containers/ContainersResource.resx | 2 ++ .../Humans.Containers/Controllers/ContainerController.cs | 3 ++- src/Sections/Humans.Containers/Docs/debt.yml | 4 ---- src/Sections/Humans.Containers/Services/Service.cs | 7 ++++--- .../Humans.Containers.Tests/Services/ServiceImageTests.cs | 8 ++++---- 10 files changed, 42 insertions(+), 12 deletions(-) diff --git a/src/Sections/Humans.Containers/ContainersResource.ca.resx b/src/Sections/Humans.Containers/ContainersResource.ca.resx index 69fb1cd979..baa3402ae5 100644 --- a/src/Sections/Humans.Containers/ContainersResource.ca.resx +++ b/src/Sections/Humans.Containers/ContainersResource.ca.resx @@ -123,6 +123,12 @@ Contenidor eliminat. + + El nom del contenidor no pot contenir <, > ni $. + + + Un contenidor pot tenir com a màxim 5 imatges. + Anterior diff --git a/src/Sections/Humans.Containers/ContainersResource.de.resx b/src/Sections/Humans.Containers/ContainersResource.de.resx index 45612a82e8..8dec425165 100644 --- a/src/Sections/Humans.Containers/ContainersResource.de.resx +++ b/src/Sections/Humans.Containers/ContainersResource.de.resx @@ -123,6 +123,12 @@ Container gelöscht. + + Der Containername darf <, > oder $ nicht enthalten. + + + Ein Container darf höchstens 5 Bilder haben. + Zurück diff --git a/src/Sections/Humans.Containers/ContainersResource.es.resx b/src/Sections/Humans.Containers/ContainersResource.es.resx index be2d7940da..8432f1f02a 100644 --- a/src/Sections/Humans.Containers/ContainersResource.es.resx +++ b/src/Sections/Humans.Containers/ContainersResource.es.resx @@ -123,6 +123,12 @@ Contenedor eliminado. + + El nombre del contenedor no puede contener <, > ni $. + + + Un contenedor puede tener como máximo 5 imágenes. + Anterior diff --git a/src/Sections/Humans.Containers/ContainersResource.fr.resx b/src/Sections/Humans.Containers/ContainersResource.fr.resx index 709dd7de5e..34332f892c 100644 --- a/src/Sections/Humans.Containers/ContainersResource.fr.resx +++ b/src/Sections/Humans.Containers/ContainersResource.fr.resx @@ -123,6 +123,12 @@ Conteneur supprimé. + + Le nom du conteneur ne doit pas contenir <, > ni $. + + + Un conteneur peut avoir au maximum 5 images. + Précédent diff --git a/src/Sections/Humans.Containers/ContainersResource.it.resx b/src/Sections/Humans.Containers/ContainersResource.it.resx index 2704d5dba3..9a38e6e7e3 100644 --- a/src/Sections/Humans.Containers/ContainersResource.it.resx +++ b/src/Sections/Humans.Containers/ContainersResource.it.resx @@ -123,6 +123,12 @@ Contenitore eliminato. + + Il nome del container non può contenere <, > o $. + + + Un container può avere al massimo 5 immagini. + Precedente diff --git a/src/Sections/Humans.Containers/ContainersResource.resx b/src/Sections/Humans.Containers/ContainersResource.resx index 02e0346d08..6b1336a69c 100644 --- a/src/Sections/Humans.Containers/ContainersResource.resx +++ b/src/Sections/Humans.Containers/ContainersResource.resx @@ -63,6 +63,8 @@ Container added. Container updated. Container deleted. + Container name must not contain <, >, or $. + A container can have at most 5 images. Previous Next diff --git a/src/Sections/Humans.Containers/Controllers/ContainerController.cs b/src/Sections/Humans.Containers/Controllers/ContainerController.cs index 3e7b806b97..afba2903b3 100644 --- a/src/Sections/Humans.Containers/Controllers/ContainerController.cs +++ b/src/Sections/Humans.Containers/Controllers/ContainerController.cs @@ -159,7 +159,8 @@ private async Task TryRunContainerWriteAsync( catch (InvalidOperationException ex) { logger.LogWarning("Container write failed for camp {Slug}: {Message}", slug, ex.Message); - SetError(ex.Message); + var localized = localizer[ex.Message]; + SetError(localized.ResourceNotFound ? ex.Message : localized.Value); return RedirectToAction(nameof(Index), new { slug }); } diff --git a/src/Sections/Humans.Containers/Docs/debt.yml b/src/Sections/Humans.Containers/Docs/debt.yml index 5715825373..8ac002be5d 100644 --- a/src/Sections/Humans.Containers/Docs/debt.yml +++ b/src/Sections/Humans.Containers/Docs/debt.yml @@ -8,7 +8,3 @@ inbox: what: "No controller tests for ContainerController (finding 13, /section-doctor on Containers 2026-09-08): the 403 on /Camp/{slug}/Containers for a non-lead, the Forbid on Create/Edit/Delete, the NotFound on an unknown slug or id, and the ModelState → flash → redirect path are unpinned end to end; the handler is unit-tested by hand-built context only. Needs the controller-test scaffolding other sections use (HumansControllerBase + IAuthorizationService substitute)." review: panel root: CENTRAL-56 - - added: 2026-09-14 - id: CONT-2 - what: "Service validation failures reach the member untranslated: ContainerController's catch hands ex.Message to SetError, so \"A container can have at most 5 images.\" and the name-character message render in English in every culture (finding 5, /section-doctor on Containers 2026-09-08). Peter 2026-09-14: accept English for now, do the error-key pass later — Service throws a key the controller localizes against ContainersResource, rather than a formatted sentence. The two messages live in Service.ValidateName and the image-count guard." - review: light diff --git a/src/Sections/Humans.Containers/Services/Service.cs b/src/Sections/Humans.Containers/Services/Service.cs index 8f5ca4f91f..f5bcaac039 100644 --- a/src/Sections/Humans.Containers/Services/Service.cs +++ b/src/Sections/Humans.Containers/Services/Service.cs @@ -25,6 +25,8 @@ internal sealed class Service( new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp" }; private const long MaxImageBytes = 10 * 1024 * 1024; private const int MaxImagesPerContainer = 5; + internal const string InvalidNameError = "Containers_Error_InvalidName"; + internal const string TooManyImagesError = "Containers_Error_TooManyImages"; public async Task> GetByCampAsync(Guid campId, CancellationToken ct = default) { @@ -284,7 +286,7 @@ private static void ValidateName(string name) { if (name.IndexOfAny(InvalidNameChars) >= 0) { - throw new InvalidOperationException("Container name must not contain <, > or $."); + throw new InvalidOperationException(InvalidNameError); } } @@ -292,8 +294,7 @@ private static void ValidateImageCount(int total) { if (total > MaxImagesPerContainer) { - throw new InvalidOperationException( - $"A container can have at most {MaxImagesPerContainer} images."); + throw new InvalidOperationException(TooManyImagesError); } } diff --git a/tests/Humans.Containers.Tests/Services/ServiceImageTests.cs b/tests/Humans.Containers.Tests/Services/ServiceImageTests.cs index c118ae56db..bef8a8414a 100644 --- a/tests/Humans.Containers.Tests/Services/ServiceImageTests.cs +++ b/tests/Humans.Containers.Tests/Services/ServiceImageTests.cs @@ -108,7 +108,7 @@ public async Task CreateAsync_RejectsMoreThanFiveImages() NewImages: FakeImages(6)), ct: TestContext.Current.CancellationToken); await act.Should().ThrowAsync() - .WithMessage("*at most 5 images*"); + .WithMessage(Service.TooManyImagesError); } [HumansFact] @@ -123,7 +123,7 @@ public async Task UpdateAsync_RejectsWhenAddedImagesWouldExceedFive() NewImages: FakeImages(2)), actorUserId: Guid.NewGuid(), ct: TestContext.Current.CancellationToken); await act.Should().ThrowAsync() - .WithMessage("*at most 5 images*"); + .WithMessage(Service.TooManyImagesError); } [HumansFact] @@ -138,7 +138,7 @@ public async Task UpdateAsync_CountsTheLegacyImageAgainstTheCap() NewImages: FakeImages(1)), actorUserId: Guid.NewGuid(), ct: TestContext.Current.CancellationToken); await act.Should().ThrowAsync() - .WithMessage("*at most 5 images*"); + .WithMessage(Service.TooManyImagesError); } [HumansFact] @@ -234,7 +234,7 @@ public async Task CreateAsync_RejectsNameWithTokenSignificantCharacters(string n Description: null), ct: TestContext.Current.CancellationToken); await act.Should().ThrowAsync() - .WithMessage("*must not contain*"); + .WithMessage(Service.InvalidNameError); } [HumansFact] From 010e513b58e4c72fa50d2f3e0d6320227cbf56d8 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:25:26 +0200 Subject: [PATCH 08/29] Move campaign tracking sort out of repository --- .../Humans.Campaigns/Data/CampaignRepository.cs | 3 +-- .../Humans.Campaigns/Data/ICampaignRepository.cs | 3 ++- .../Humans.Campaigns/Services/CampaignService.cs | 1 + .../Services/CampaignServiceTests.cs | 12 ++++++++++++ .../Baselines/DisplaySortInControllers.baseline.txt | 1 - 5 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Sections/Humans.Campaigns/Data/CampaignRepository.cs b/src/Sections/Humans.Campaigns/Data/CampaignRepository.cs index 16ecaf84e8..255720e6dc 100644 --- a/src/Sections/Humans.Campaigns/Data/CampaignRepository.cs +++ b/src/Sections/Humans.Campaigns/Data/CampaignRepository.cs @@ -61,8 +61,7 @@ public async Task> GetCodeTracking return await ctx.Campaigns .AsNoTracking() .Where(c => c.Status == CampaignStatus.Active || c.Status == CampaignStatus.Completed) - .OrderByDescending(c => c.CreatedAt) - .Select(c => new CampaignCodeTrackingSummaryRow(c.Id, c.Title)) + .Select(c => new CampaignCodeTrackingSummaryRow(c.Id, c.Title, c.CreatedAt)) .ToListAsync(ct); } diff --git a/src/Sections/Humans.Campaigns/Data/ICampaignRepository.cs b/src/Sections/Humans.Campaigns/Data/ICampaignRepository.cs index f966ca0bf3..de69c5a240 100644 --- a/src/Sections/Humans.Campaigns/Data/ICampaignRepository.cs +++ b/src/Sections/Humans.Campaigns/Data/ICampaignRepository.cs @@ -183,7 +183,8 @@ internal sealed record GrantWithSendContext( /// internal sealed record CampaignCodeTrackingSummaryRow( Guid CampaignId, - string CampaignTitle); + string CampaignTitle, + Instant CreatedAt); /// /// One grant per row, used by . diff --git a/src/Sections/Humans.Campaigns/Services/CampaignService.cs b/src/Sections/Humans.Campaigns/Services/CampaignService.cs index ee90ebe63f..5c9d4954dc 100644 --- a/src/Sections/Humans.Campaigns/Services/CampaignService.cs +++ b/src/Sections/Humans.Campaigns/Services/CampaignService.cs @@ -574,6 +574,7 @@ public async Task GetCodeTrackingAsync(CancellationTok .ToDictionary(g => g.Key, g => g.ToList()); var summaries = summaryRows + .OrderByDescending(s => s.CreatedAt) .Select(s => { grantsByCampaign.TryGetValue(s.CampaignId, out var grants); diff --git a/tests/Humans.Campaigns.Tests/Services/CampaignServiceTests.cs b/tests/Humans.Campaigns.Tests/Services/CampaignServiceTests.cs index 755dadb0f3..d59c5eed75 100644 --- a/tests/Humans.Campaigns.Tests/Services/CampaignServiceTests.cs +++ b/tests/Humans.Campaigns.Tests/Services/CampaignServiceTests.cs @@ -741,6 +741,18 @@ await CampaignsDb.CampaignGrants.AddAsync(new CampaignGrant grant.Code == "TRACK-CODE" && grant.RedeemedAt == redeemedAt && grant.LatestEmailStatus == "Sent"); } + [HumansFact] + public async Task GetCodeTrackingAsync_ordersCampaignsNewestFirst() + { + var older = await SeedActiveCampaignWithCodesAsync(["OLDER"]); + Clock.AdvanceHours(1); + var newer = await SeedActiveCampaignWithCodesAsync(["NEWER"]); + + var tracking = await _service.GetCodeTrackingAsync(Xunit.TestContext.Current.CancellationToken); + + tracking.Campaigns.Select(c => c.CampaignId).Should().Equal(newer.Id, older.Id); + } + [HumansFact] public async Task EraseForUserAsync_DeletesOnlyThatUsersGrants() { diff --git a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt index 99232c8336..bdf5350ad1 100644 --- a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt +++ b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt @@ -31,4 +31,3 @@ src/Sections/Humans.Tickets/Data/TicketRepository.cs:OrderByDescending#6 src/Sections/Humans.Tickets/Data/TicketRepository.cs:OrderByDescending#7 src/Sections/Humans.Tickets/Data/TicketRepository.cs:OrderByDescending#8 src/Sections/Humans.Tickets/Data/TicketRepository.cs:OrderByDescending#9 -src/Sections/Humans.Campaigns/Data/CampaignRepository.cs:OrderByDescending#1 From a4977e5a9c70b58cf23eda80789fafebf3b45a35 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:28:32 +0200 Subject: [PATCH 09/29] Move Google resource sorting out of repository --- .../Humans.GoogleIntegration/Data/GoogleResourceRepository.cs | 2 -- .../Humans.GoogleIntegration/Services/TeamResourceService.cs | 2 +- .../GoogleResourceRepositoryTests.cs | 4 ++-- .../Baselines/DisplaySortInControllers.baseline.txt | 2 -- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs b/src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs index 8e8ec86167..3b4ea45f4a 100644 --- a/src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs +++ b/src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs @@ -27,7 +27,6 @@ public async Task> GetActiveByTeamIdAsync(Guid tea return await ctx.GoogleResources .AsNoTracking() .Where(r => r.TeamId == teamId && r.IsActive) - .OrderBy(r => r.ProvisionedAt) .ToListAsync(ct); } @@ -44,7 +43,6 @@ public async Task>> GetA var rows = await ctx.GoogleResources .AsNoTracking() .Where(r => teamIds.Contains(r.TeamId) && r.IsActive) - .OrderBy(r => r.ProvisionedAt) .ToListAsync(ct); var result = new Dictionary>(teamIds.Count); diff --git a/src/Sections/Humans.GoogleIntegration/Services/TeamResourceService.cs b/src/Sections/Humans.GoogleIntegration/Services/TeamResourceService.cs index a76d7558f4..cb86933138 100644 --- a/src/Sections/Humans.GoogleIntegration/Services/TeamResourceService.cs +++ b/src/Sections/Humans.GoogleIntegration/Services/TeamResourceService.cs @@ -34,7 +34,7 @@ private IRoleAssignmentService RoleAssignmentService public async Task> GetTeamResourcesAsync(Guid teamId, CancellationToken ct = default) { var resources = await repository.GetActiveByTeamIdAsync(teamId, ct); - return resources.Select(ToSnapshot).ToList(); + return resources.OrderBy(r => r.ProvisionedAt).Select(ToSnapshot).ToList(); } public async Task>> GetResourcesByTeamIdsAsync( diff --git a/tests/Humans.GoogleIntegration.Tests/GoogleResourceRepositoryTests.cs b/tests/Humans.GoogleIntegration.Tests/GoogleResourceRepositoryTests.cs index 94fb9688c3..398aef02f7 100644 --- a/tests/Humans.GoogleIntegration.Tests/GoogleResourceRepositoryTests.cs +++ b/tests/Humans.GoogleIntegration.Tests/GoogleResourceRepositoryTests.cs @@ -46,7 +46,7 @@ public void Dispose() } [HumansFact] - public async Task GetActiveByTeamIdAsync_OrdersByProvisionedAt_ExcludesInactive() + public async Task GetActiveByTeamIdAsync_ExcludesInactive() { var teamId = Guid.NewGuid(); var older = Seed(teamId, "older", GoogleResourceType.DriveFolder, Instant.FromUtc(2026, 4, 20, 0, 0)); @@ -57,7 +57,7 @@ public async Task GetActiveByTeamIdAsync_OrdersByProvisionedAt_ExcludesInactive( var rows = await _repository.GetActiveByTeamIdAsync(teamId, Xunit.TestContext.Current.CancellationToken); rows.Should().HaveCount(2); - rows.Select(r => r.Id).Should().ContainInOrder(older.Id, newer.Id); + rows.Select(r => r.Id).Should().BeEquivalentTo([older.Id, newer.Id]); } [HumansFact] diff --git a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt index bdf5350ad1..a19f551a33 100644 --- a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt +++ b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt @@ -8,8 +8,6 @@ src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:OrderBy#1 src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:ThenBy#1 -src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs:OrderBy#1 -src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs:OrderBy#2 src/Sections/Humans.Teams/Data/TeamRepository.cs:OrderBy#1 src/Sections/Humans.Teams/Data/TeamRepository.cs:ThenBy#1 src/Sections/Humans.Teams/Data/TeamRepository.cs:ThenBy#2 From 9ecf0a4a6b229807832a1599072ec9958a88dc93 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:32:56 +0200 Subject: [PATCH 10/29] Localize team edit page --- src/Sections/Humans.Teams/Docs/debt.yml | 4 --- .../Humans.Teams/TeamsResource.ca.resx | 15 ++++++++++ .../Humans.Teams/TeamsResource.de.resx | 15 ++++++++++ .../Humans.Teams/TeamsResource.es.resx | 15 ++++++++++ .../Humans.Teams/TeamsResource.fr.resx | 15 ++++++++++ .../Humans.Teams/TeamsResource.it.resx | 15 ++++++++++ src/Sections/Humans.Teams/TeamsResource.resx | 15 ++++++++++ .../Humans.Teams/Views/Team/EditTeam.cshtml | 28 +++++++++---------- 8 files changed, 104 insertions(+), 18 deletions(-) diff --git a/src/Sections/Humans.Teams/Docs/debt.yml b/src/Sections/Humans.Teams/Docs/debt.yml index 28e6892484..f5b071d735 100644 --- a/src/Sections/Humans.Teams/Docs/debt.yml +++ b/src/Sections/Humans.Teams/Docs/debt.yml @@ -11,10 +11,6 @@ inbox: id: TEAMS-2 what: "Services/TeamService.cs is the section's largest class (reforge largeClass), with UpdateTeamAsync its most complex method over a long parameter list, and Data/Configurations/TeamConfiguration.cs Configure is a single long method. The target shape blesses one inner service; recorded so the score is not mistaken for unexamined debt (finding 12, /section-doctor on Teams 2026-09-05)." review: panel - - added: 2026-09-05 - id: TEAMS-3 - what: "Views/TeamAdmin/EditTeam.cshtml carries hardcoded English strings on a member-facing coordinator route (/Teams/{slug}/Edit is not in localization-admin-exempt's list); each needs a TeamsResource key in every supported culture (finding 12, /section-doctor on Teams 2026-09-05)." - review: light - added: 2026-09-05 id: TEAMS-4 what: "Most TeamsResource keys lack the Teams_ prefix (TeamDetail_*, TeamAdmin_*, MyTeams_*, AdminEditTeam_*); pre-existing baseline, report only — a rename is a six-culture sweep plus every view binding (finding 12, /section-doctor on Teams 2026-09-05)." diff --git a/src/Sections/Humans.Teams/TeamsResource.ca.resx b/src/Sections/Humans.Teams/TeamsResource.ca.resx index 67c2168621..811949a3a5 100644 --- a/src/Sections/Humans.Teams/TeamsResource.ca.resx +++ b/src/Sections/Humans.Teams/TeamsResource.ca.resx @@ -534,4 +534,19 @@ <p>El teu equip té els recursos següents:</p><ul>{0}</ul> {0} = resource list items HTML + Aquest és un equip gestionat pel sistema. Pots editar la descripció i el prefix del grup de Google. + Slug generat automàticament: + s'actualitza automàticament quan canvia el nom. + Slug personalitzat + p. ex., comunicacio + Opcional. Quan s'estableix, aquest slug s'utilitza per a l'URL de l'equip en lloc del generat automàticament. + Tots dos slugs resoldran a aquest equip. Només lletres minúscules, números i guionets. + Inclou en la planificació pressupostària + En activar-ho, aquest equip obté una categoria pressupostària sota el grup de Departaments en crear un nou any pressupostari. + Mostra a la pàgina d'equips + En activar-ho, aquest subequip també apareix al directori principal d'equips. + Equip sensible + En activar-ho, afegir o aprovar persones mostra un diàleg de confirmació amb el registre d'auditoria que es crearà. Aquesta opció no és visible per a usuaris que no són administradors. + Activa l'entrada anticipada + En activar-ho, els coordinadors d'aquest equip i els administradors d'entrada anticipada poden concedir entrada anticipada als projectes d'aquest equip. diff --git a/src/Sections/Humans.Teams/TeamsResource.de.resx b/src/Sections/Humans.Teams/TeamsResource.de.resx index 193a5dd016..c8059c3b18 100644 --- a/src/Sections/Humans.Teams/TeamsResource.de.resx +++ b/src/Sections/Humans.Teams/TeamsResource.de.resx @@ -534,4 +534,19 @@ <p>Dein Team hat folgende Ressourcen:</p><ul>{0}</ul> {0} = resource list items HTML + Dies ist ein systemverwaltetes Team. Du kannst die Beschreibung und das Präfix der Google-Gruppe bearbeiten. + Automatisch erzeugter Slug: + wird bei einer Namensänderung automatisch aktualisiert. + Benutzerdefinierter Slug + z. B. kommunikation + Optional. Wenn gesetzt, wird dieser Slug statt des automatisch erzeugten für die Team-URL verwendet. + Beide Slugs führen zu diesem Team. Nur Kleinbuchstaben, Zahlen und Bindestriche. + In Budgetplanung einbeziehen + Wenn aktiviert, erhält dieses Team beim Erstellen eines neuen Haushaltsjahres eine Budgetkategorie unter der Gruppe Abteilungen. + Auf der Teamseite anzeigen + Wenn aktiviert, erscheint dieses Unterteam auch im Hauptverzeichnis der Teams. + Sensibles Team + Wenn aktiviert, zeigt das Hinzufügen oder Genehmigen von Personen einen Bestätigungsdialog mit dem zu erstellenden Prüfprotokoll. Diese Einstellung ist für Nicht-Administratoren nicht sichtbar. + Frühen Einlass aktivieren + Wenn aktiviert, können die Koordinatoren dieses Teams und die Administratoren für frühen Einlass den Projekten dieses Teams frühen Einlass gewähren. diff --git a/src/Sections/Humans.Teams/TeamsResource.es.resx b/src/Sections/Humans.Teams/TeamsResource.es.resx index 48f4de8db9..a639cde4bb 100644 --- a/src/Sections/Humans.Teams/TeamsResource.es.resx +++ b/src/Sections/Humans.Teams/TeamsResource.es.resx @@ -534,4 +534,19 @@ <p>Tu equipo tiene los siguientes recursos:</p><ul>{0}</ul> {0} = resource list items HTML + Este es un equipo gestionado por el sistema. Puedes editar la descripción y el prefijo del grupo de Google. + Slug generado automáticamente: + se actualiza automáticamente cuando cambia el nombre. + Slug personalizado + p. ej., comunicacion + Opcional. Cuando se establece, este slug se usa para la URL del equipo en lugar del generado automáticamente. + Ambos slugs resolverán a este equipo. Solo letras minúsculas, números y guiones. + Incluir en la planificación presupuestaria + Al activarlo, este equipo obtiene una categoría presupuestaria bajo el grupo de Departamentos al crear un nuevo año presupuestario. + Mostrar en la página de equipos + Al activarlo, este subequipo también aparece en el directorio principal de equipos. + Equipo sensible + Al activarlo, añadir o aprobar personas muestra un diálogo de confirmación con el registro de auditoría que se creará. Esta opción no es visible para usuarios que no sean administradores. + Activar acceso anticipado + Al activarlo, los coordinadores de este equipo y los administradores de acceso anticipado pueden conceder acceso anticipado a los proyectos de este equipo. diff --git a/src/Sections/Humans.Teams/TeamsResource.fr.resx b/src/Sections/Humans.Teams/TeamsResource.fr.resx index 5a719cfff6..872fae20f1 100644 --- a/src/Sections/Humans.Teams/TeamsResource.fr.resx +++ b/src/Sections/Humans.Teams/TeamsResource.fr.resx @@ -534,4 +534,19 @@ <p>Votre équipe dispose des ressources suivantes :</p><ul>{0}</ul> {0} = resource list items HTML + Cette équipe est gérée par le système. Vous pouvez modifier la description et le préfixe du groupe Google. + Slug généré automatiquement : + se met à jour automatiquement lorsque le nom change. + Slug personnalisé + p. ex. communication + Facultatif. Lorsqu'il est défini, ce slug est utilisé pour l'URL de l'équipe à la place de celui généré automatiquement. + Les deux slugs renverront vers cette équipe. Lettres minuscules, chiffres et traits d'union uniquement. + Inclure dans la planification budgétaire + Lorsqu'elle est activée, cette équipe obtient une catégorie budgétaire dans le groupe Départements lors de la création d'une nouvelle année budgétaire. + Afficher sur la page des équipes + Lorsqu'elle est activée, cette sous-équipe apparaît aussi dans l'annuaire principal des équipes. + Équipe sensible + Lorsqu'elle est activée, l'ajout ou l'approbation de personnes affiche une confirmation avec l'enregistrement d'audit qui sera créé. Cette option n'est pas visible pour les non-administrateurs. + Activer l'entrée anticipée + Lorsqu'elle est activée, les coordinateurs de cette équipe et les administrateurs d'entrée anticipée peuvent accorder une entrée anticipée aux projets de cette équipe. diff --git a/src/Sections/Humans.Teams/TeamsResource.it.resx b/src/Sections/Humans.Teams/TeamsResource.it.resx index 961bb88947..1d0a5f30e7 100644 --- a/src/Sections/Humans.Teams/TeamsResource.it.resx +++ b/src/Sections/Humans.Teams/TeamsResource.it.resx @@ -534,4 +534,19 @@ <p>Il tuo team ha le seguenti risorse:</p><ul>{0}</ul> {0} = resource list items HTML + Questo è un team gestito dal sistema. Puoi modificare la descrizione e il prefisso del gruppo Google. + Slug generato automaticamente: + si aggiorna automaticamente quando cambia il nome. + Slug personalizzato + ad es. comunicazione + Facoltativo. Quando impostato, questo slug viene usato per l'URL del team al posto di quello generato automaticamente. + Entrambi gli slug risolveranno a questo team. Solo lettere minuscole, numeri e trattini. + Includi nella pianificazione del budget + Quando attivato, questo team ottiene una categoria di budget nel gruppo Dipartimenti alla creazione di un nuovo anno di bilancio. + Mostra nella pagina dei team + Quando attivato, questo sotto-team appare anche nella directory principale dei team. + Team sensibile + Quando attivato, l'aggiunta o l'approvazione di persone mostra una conferma con il registro di controllo che verrà creato. Questa opzione non è visibile agli utenti non amministratori. + Abilita l'ingresso anticipato + Quando attivato, i coordinatori di questo team e gli amministratori dell'ingresso anticipato possono concedere l'ingresso anticipato ai progetti di questo team. diff --git a/src/Sections/Humans.Teams/TeamsResource.resx b/src/Sections/Humans.Teams/TeamsResource.resx index ed3273c579..2b527d9f5c 100644 --- a/src/Sections/Humans.Teams/TeamsResource.resx +++ b/src/Sections/Humans.Teams/TeamsResource.resx @@ -212,4 +212,19 @@ <p><a href="{3}">View Team Page</a></p> <p>The Humans Team</p>{0} = user name, {1} = team name, {2} = resources HTML (or empty), {3} = team URL <p>Your team has the following resources:</p><ul>{0}</ul>{0} = resource list items HTML + This is a system-managed team. You can edit the description and Google Group prefix. + Auto-generated slug: + updates automatically when name changes. + Custom Slug + e.g. comms + Optional. When set, this slug is used for the team's URL instead of the auto-generated one. + Both slugs will resolve to this team. Lowercase letters, numbers, and hyphens only. + Include in budget planning + When enabled, this team gets a budget category under the Departments group when a new budget year is created. + Show on Teams page + When enabled, this sub-team also appears on the main Teams directory page. + Sensitive team + When enabled, adding or approving humans triggers a confirmation modal showing the audit record that will be created. This flag is not visible to non-admin users. + Enable Early Entry + When enabled, coordinators of this team (and Early-Entry Team Admins) can grant early entry for this team's projects. diff --git a/src/Sections/Humans.Teams/Views/Team/EditTeam.cshtml b/src/Sections/Humans.Teams/Views/Team/EditTeam.cshtml index a6b152bae2..0c082df84e 100644 --- a/src/Sections/Humans.Teams/Views/Team/EditTeam.cshtml +++ b/src/Sections/Humans.Teams/Views/Team/EditTeam.cshtml @@ -22,7 +22,7 @@ {
- This is a system-managed team. You can edit the description and Google Group prefix. + @Localizer["AdminEditTeam_SystemManagedHelp"]
} @@ -37,18 +37,18 @@ -
Auto-generated slug: @Model.Slug — updates automatically when name changes.
+
@Localizer["AdminEditTeam_AutoSlug"] @Model.Slug — @Localizer["AdminEditTeam_AutoSlugHelp"]
@if (!Model.IsSystemTeam) {
- - + +
- Optional. When set, this slug is used for the team's URL instead of the auto-generated one. - Both slugs will resolve to this team. Lowercase letters, numbers, and hyphens only. + @Localizer["AdminEditTeam_CustomSlugHelp"] + @Localizer["AdminEditTeam_CustomSlugFormatHelp"]
} @@ -64,8 +64,8 @@
- -
When enabled, this team gets a budget category under the Departments group when a new budget year is created.
+ +
@Localizer["AdminEditTeam_IncludeInBudgetHelp"]
} @@ -73,21 +73,21 @@ {
- -
When enabled, this sub-team also appears on the main Teams directory page.
+ +
@Localizer["AdminEditTeam_ShowInDirectoryHelp"]
}
- -
When enabled, adding or approving humans triggers a confirmation modal showing the audit record that will be created. This flag is not visible to non-admin users.
+ +
@Localizer["AdminEditTeam_SensitiveHelp"]
- -
When enabled, coordinators of this team (and Early-Entry Team Admins) can grant early entry for this team's projects.
+ +
@Localizer["AdminEditTeam_EnableEarlyEntryHelp"]
From 25e57db1cf3a9586753be3a86728541d429ff66e Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:37:28 +0200 Subject: [PATCH 11/29] Localize individual event form --- .../Humans.Events/EventsResource.ca.resx | 19 +++++++++ .../Humans.Events/EventsResource.de.resx | 19 +++++++++ .../Humans.Events/EventsResource.es.resx | 19 +++++++++ .../Humans.Events/EventsResource.fr.resx | 19 +++++++++ .../Humans.Events/EventsResource.it.resx | 19 +++++++++ .../Humans.Events/EventsResource.resx | 19 +++++++++ .../Views/Events/IndividualEventForm.cshtml | 40 +++++++++---------- 7 files changed, 134 insertions(+), 20 deletions(-) diff --git a/src/Sections/Humans.Events/EventsResource.ca.resx b/src/Sections/Humans.Events/EventsResource.ca.resx index 61d3bc2b89..958d1b93ef 100644 --- a/src/Sections/Humans.Events/EventsResource.ca.resx +++ b/src/Sections/Humans.Events/EventsResource.ca.resx @@ -93,4 +93,23 @@ La pujada ha fallat. Corregeix els errors de sota i torna-ho a provar. No s'ha desat cap esdeveniment. Vols retirar aquest esdeveniment? + Edita l'esdeveniment + Envia un esdeveniment + Edita + Nou + Aquest esdeveniment s'ha retornat per editar-lo. Actualitza'n els detalls i torna'l a enviar. + caràcters restants + S'admet el format Markdown + — Selecciona una categoria — + — Selecciona un lloc — + Esdeveniment de tot el dia + — Selecciona un dia — + Aquest esdeveniment es repeteix diversos dies + Dies de repetició + p. ex., prop de la foguera + p. ex., El Col·lectiu del Te + Es mostra a la guia en lloc del teu nom quan s'omple. + Torna a enviar + Envia per revisar + Cancel·la diff --git a/src/Sections/Humans.Events/EventsResource.de.resx b/src/Sections/Humans.Events/EventsResource.de.resx index d8d348023c..76bf9134e3 100644 --- a/src/Sections/Humans.Events/EventsResource.de.resx +++ b/src/Sections/Humans.Events/EventsResource.de.resx @@ -93,4 +93,23 @@ Upload fehlgeschlagen. Behebe die folgenden Fehler und versuche es erneut. Es wurden keine Veranstaltungen gespeichert. Diese Veranstaltung zurückziehen? + Veranstaltung bearbeiten + Veranstaltung einreichen + Bearbeiten + Neu + Diese Veranstaltung wurde zur Bearbeitung zurückgegeben. Aktualisiere die Angaben und reiche sie erneut ein. + Zeichen verbleiben + Markdown-Formatierung wird unterstützt + — Kategorie auswählen — + — Ort auswählen — + Ganztägige Veranstaltung + — Tag auswählen — + Diese Veranstaltung wiederholt sich an mehreren Tagen + Wiederholungstage + z. B. nahe der Feuerstelle + z. B. das Teekollektiv + Wird ausgefüllt im Guide statt deines Namens angezeigt. + Erneut einreichen + Zur Prüfung einreichen + Abbrechen diff --git a/src/Sections/Humans.Events/EventsResource.es.resx b/src/Sections/Humans.Events/EventsResource.es.resx index 62de58d97a..25431e37d1 100644 --- a/src/Sections/Humans.Events/EventsResource.es.resx +++ b/src/Sections/Humans.Events/EventsResource.es.resx @@ -93,4 +93,23 @@ La subida falló. Corrige los errores de abajo e inténtalo de nuevo. No se guardó ningún evento. ¿Retirar este evento? + Editar evento + Enviar un evento + Editar + Nuevo + Este evento se devolvió para editarlo. Actualiza los detalles y vuelve a enviarlo. + caracteres restantes + Se admite formato Markdown + — Selecciona una categoría — + — Selecciona un lugar — + Evento de todo el día + — Selecciona un día — + Este evento se repite varios días + Días de repetición + p. ej., cerca de la hoguera + p. ej., El Colectivo del Té + Se muestra en la guía en lugar de tu nombre cuando se completa. + Volver a enviar + Enviar para revisión + Cancelar diff --git a/src/Sections/Humans.Events/EventsResource.fr.resx b/src/Sections/Humans.Events/EventsResource.fr.resx index 769c8ab52a..1e4f16a30e 100644 --- a/src/Sections/Humans.Events/EventsResource.fr.resx +++ b/src/Sections/Humans.Events/EventsResource.fr.resx @@ -93,4 +93,23 @@ Échec du téléversement. Corrigez les erreurs ci-dessous et réessayez. Aucun événement n'a été enregistré. Retirer cet événement ? + Modifier l'événement + Proposer un événement + Modifier + Nouveau + Cet événement a été renvoyé pour modification. Mettez les détails à jour et soumettez-le à nouveau. + caractères restants + Le formatage Markdown est pris en charge + — Sélectionner une catégorie — + — Sélectionner un lieu — + Événement sur toute la journée + — Sélectionner un jour — + Cet événement se répète sur plusieurs jours + Jours de répétition + p. ex. près du feu de camp + p. ex. le Collectif du Thé + S'affiche dans le guide à la place de votre nom lorsqu'il est renseigné. + Soumettre à nouveau + Soumettre pour examen + Annuler diff --git a/src/Sections/Humans.Events/EventsResource.it.resx b/src/Sections/Humans.Events/EventsResource.it.resx index 5d510979d5..9f4ecca8c3 100644 --- a/src/Sections/Humans.Events/EventsResource.it.resx +++ b/src/Sections/Humans.Events/EventsResource.it.resx @@ -93,4 +93,23 @@ Caricamento fallito. Correggi gli errori qui sotto e riprova. Nessun evento è stato salvato. Ritirare questo evento? + Modifica evento + Invia un evento + Modifica + Nuovo + Questo evento è stato restituito per le modifiche. Aggiorna i dettagli e invialo di nuovo. + caratteri rimanenti + La formattazione Markdown è supportata + — Seleziona una categoria — + — Seleziona un luogo — + Evento per tutto il giorno + — Seleziona un giorno — + Questo evento si ripete in più giorni + Giorni di ripetizione + ad es. vicino al falò + ad es. il Collettivo del Tè + Viene mostrato nella guida al posto del tuo nome quando compilato. + Invia di nuovo + Invia per revisione + Annulla diff --git a/src/Sections/Humans.Events/EventsResource.resx b/src/Sections/Humans.Events/EventsResource.resx index aa4f4c08b4..6c896c8e16 100644 --- a/src/Sections/Humans.Events/EventsResource.resx +++ b/src/Sections/Humans.Events/EventsResource.resx @@ -88,4 +88,23 @@ Upload failed. Fix the errors below and try again. No events were saved. Withdraw this event? + Edit Event + Submit an Event + Edit + New + This event was returned for edits. Update the details and resubmit. + characters remaining + Markdown formatting is supported + — Select category — + — Select venue — + All day event + — Select day — + This event repeats on multiple days + Recurrence days + e.g. near the fire pit + e.g. The Tea Collective + Shown in the guide instead of your name when filled in. + Resubmit + Submit for Review + Cancel diff --git a/src/Sections/Humans.Events/Views/Events/IndividualEventForm.cshtml b/src/Sections/Humans.Events/Views/Events/IndividualEventForm.cshtml index 5be4e99c7d..ada82a5ee5 100644 --- a/src/Sections/Humans.Events/Views/Events/IndividualEventForm.cshtml +++ b/src/Sections/Humans.Events/Views/Events/IndividualEventForm.cshtml @@ -1,29 +1,29 @@ @model Humans.Events.Models.IndividualEventFormViewModel @{ var isEdit = Model.Id.HasValue; - ViewData["Title"] = isEdit ? "Edit Event" : "Submit an Event"; + ViewData["Title"] = isEdit ? Localizer["EventSubmission_EditTitle"].Value : Localizer["EventSubmission_SubmitTitle"].Value; } -

@(isEdit ? "Edit Event" : "Submit an Event")

+

@(isEdit ? Localizer["EventSubmission_EditTitle"] : Localizer["EventSubmission_SubmitTitle"])

@if (Model.IsResubmit) {
- This event was returned for edits. Update the details and resubmit. + @Localizer["EventSubmission_ReturnedForEdits"]
} @if (Model.TimeZoneId != null) {

- All times in @Model.TimeZoneId. Playa Time may apply! + @Localizer["Events_AllTimesIn", Model.TimeZoneId]

} @@ -38,17 +38,17 @@
-
@(80 - (Model.Title?.Length ?? 0)) characters remaining
+
@(80 - (Model.Title?.Length ?? 0)) @Localizer["EventSubmission_CharactersRemaining"]
-
@(450 - (Model.Description?.Length ?? 0)) characters remaining
+
@(450 - (Model.Description?.Length ?? 0)) @Localizer["EventSubmission_CharactersRemaining"]
@@ -57,7 +57,7 @@
- + @foreach (var venue in Model.Venues) { @@ -80,14 +80,14 @@
- +
- +
From 566313ed6fed50a7b9aab27f90329335bc6fc90f Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:41:08 +0200 Subject: [PATCH 12/29] Localize event submission surfaces --- src/Sections/Humans.Events/Docs/debt.yml | 4 -- .../Humans.Events/EventsResource.ca.resx | 8 ++++ .../Humans.Events/EventsResource.de.resx | 8 ++++ .../Humans.Events/EventsResource.es.resx | 8 ++++ .../Humans.Events/EventsResource.fr.resx | 8 ++++ .../Humans.Events/EventsResource.it.resx | 8 ++++ .../Humans.Events/EventsResource.resx | 8 ++++ .../Views/Events/BarrioEventForm.cshtml | 40 ++++++++++--------- .../Components/EventsCard/Default.cshtml | 4 +- 9 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/Sections/Humans.Events/Docs/debt.yml b/src/Sections/Humans.Events/Docs/debt.yml index 35d254b5e3..ef5f31e29c 100644 --- a/src/Sections/Humans.Events/Docs/debt.yml +++ b/src/Sections/Humans.Events/Docs/debt.yml @@ -28,7 +28,3 @@ inbox: id: EVENTS-6 what: "EventsApiController's day-offset path always runs with tz == null because the shared test ctor stubs guide settings to null (EventsApiControllerTests.cs:28-32), so the non-null branch is never executed. Found by /section-doctor on Events 2026-09-09. (split 2026-09-20 from the bundled row of the same date; one row per independently-fixable defect.)" review: light - - added: 2026-09-09 - id: EVENTS-7 - what: "Member-facing views ship hardcoded English: Views/Events/IndividualEventForm.cshtml (no Localizer reference at all), Views/Events/BarrioEventForm.cshtml (a stray Localizer reference aside, its labels, hints and buttons are literals) and Views/Shared/Components/EventsCard/Default.cshtml ('Events' header, 'Recurring event' tooltip). Browse, Schedule and MySubmissions are fully localized; the submit forms never were. Fix is a resx backfill across every supported culture plus the view edits — its own PR, not a side effect. Found by /section-doctor on Events 2026-09-09." - review: light diff --git a/src/Sections/Humans.Events/EventsResource.ca.resx b/src/Sections/Humans.Events/EventsResource.ca.resx index 958d1b93ef..dcd9351a71 100644 --- a/src/Sections/Humans.Events/EventsResource.ca.resx +++ b/src/Sections/Humans.Events/EventsResource.ca.resx @@ -112,4 +112,12 @@ Torna a enviar Envia per revisar Cancel·la + Edita l'esdeveniment — {0} + Envia l'esdeveniment — {0} + Envia un esdeveniment nou + p. ex., Luna + Opcional: indica la persona que organitza aquest esdeveniment. + 1 = prioritat més alta per a la selecció de la guia impresa. + Esdeveniments + Esdeveniment recurrent diff --git a/src/Sections/Humans.Events/EventsResource.de.resx b/src/Sections/Humans.Events/EventsResource.de.resx index 76bf9134e3..44cab8ce7b 100644 --- a/src/Sections/Humans.Events/EventsResource.de.resx +++ b/src/Sections/Humans.Events/EventsResource.de.resx @@ -112,4 +112,12 @@ Erneut einreichen Zur Prüfung einreichen Abbrechen + Veranstaltung bearbeiten — {0} + Veranstaltung einreichen — {0} + Neue Veranstaltung einreichen + z. B. Luna + Optional — benennt die Person, die diese Veranstaltung leitet. + 1 = höchste Priorität für die Auswahl im gedruckten Guide. + Veranstaltungen + Wiederkehrende Veranstaltung diff --git a/src/Sections/Humans.Events/EventsResource.es.resx b/src/Sections/Humans.Events/EventsResource.es.resx index 25431e37d1..2e9921642f 100644 --- a/src/Sections/Humans.Events/EventsResource.es.resx +++ b/src/Sections/Humans.Events/EventsResource.es.resx @@ -112,4 +112,12 @@ Volver a enviar Enviar para revisión Cancelar + Editar evento — {0} + Enviar evento — {0} + Enviar nuevo evento + p. ej., Luna + Opcional; indica la persona que organiza este evento. + 1 = prioridad máxima para la selección de la guía impresa. + Eventos + Evento recurrente diff --git a/src/Sections/Humans.Events/EventsResource.fr.resx b/src/Sections/Humans.Events/EventsResource.fr.resx index 1e4f16a30e..405d2fcda7 100644 --- a/src/Sections/Humans.Events/EventsResource.fr.resx +++ b/src/Sections/Humans.Events/EventsResource.fr.resx @@ -112,4 +112,12 @@ Soumettre à nouveau Soumettre pour examen Annuler + Modifier l'événement — {0} + Proposer l'événement — {0} + Proposer un nouvel événement + p. ex. Luna + Facultatif — indique la personne qui anime cet événement. + 1 = priorité la plus élevée pour la sélection du guide imprimé. + Événements + Événement récurrent diff --git a/src/Sections/Humans.Events/EventsResource.it.resx b/src/Sections/Humans.Events/EventsResource.it.resx index 9f4ecca8c3..28f1e9a0f8 100644 --- a/src/Sections/Humans.Events/EventsResource.it.resx +++ b/src/Sections/Humans.Events/EventsResource.it.resx @@ -112,4 +112,12 @@ Invia di nuovo Invia per revisione Annulla + Modifica evento — {0} + Invia evento — {0} + Invia nuovo evento + ad es. Luna + Facoltativo: indica la persona che gestisce questo evento. + 1 = priorità più alta per la selezione della guida stampata. + Eventi + Evento ricorrente diff --git a/src/Sections/Humans.Events/EventsResource.resx b/src/Sections/Humans.Events/EventsResource.resx index 6c896c8e16..7512cc62a8 100644 --- a/src/Sections/Humans.Events/EventsResource.resx +++ b/src/Sections/Humans.Events/EventsResource.resx @@ -107,4 +107,12 @@ Resubmit Submit for Review Cancel + Edit Event — {0} + Submit Event — {0} + Submit New Event + e.g. Luna + Optional — names the person running this event. + 1 = highest priority for print guide selection. + Events + Recurring event diff --git a/src/Sections/Humans.Events/Views/Events/BarrioEventForm.cshtml b/src/Sections/Humans.Events/Views/Events/BarrioEventForm.cshtml index 5c845bdd07..97544237d9 100644 --- a/src/Sections/Humans.Events/Views/Events/BarrioEventForm.cshtml +++ b/src/Sections/Humans.Events/Views/Events/BarrioEventForm.cshtml @@ -1,31 +1,33 @@ @model Humans.Events.Models.CampEventFormViewModel @{ var isEdit = Model.Id.HasValue; - ViewData["Title"] = isEdit ? $"Edit Event — {Model.CampName}" : $"Submit Event — {Model.CampName}"; + ViewData["Title"] = isEdit + ? Localizer["EventSubmission_BarrioEditTitle", Model.CampName].Value + : Localizer["EventSubmission_BarrioSubmitTitle", Model.CampName].Value; } -

@(isEdit ? "Edit Event" : "Submit New Event")

+

@(isEdit ? Localizer["EventSubmission_EditTitle"] : Localizer["EventSubmission_SubmitNew"])

@if (Model.IsResubmit) {
- This event was returned for edits. Update the details and resubmit. + @Localizer["EventSubmission_ReturnedForEdits"]
} @if (Model.TimeZoneId != null) {

- All times in @Model.TimeZoneId + @Localizer["Events_AllTimesInShort", Model.TimeZoneId]

} @@ -41,17 +43,17 @@
-
@(80 - (Model.Title?.Length ?? 0)) characters remaining
+
@(80 - (Model.Title?.Length ?? 0)) @Localizer["EventSubmission_CharactersRemaining"]
-
@(450 - (Model.Description?.Length ?? 0)) characters remaining
+
@(450 - (Model.Description?.Length ?? 0)) @Localizer["EventSubmission_CharactersRemaining"]
@@ -60,7 +62,7 @@
- + @foreach (var day in Model.EventDays) { @@ -105,11 +107,11 @@
- +
diff --git a/src/Sections/Humans.Events/Views/Shared/Components/EventsCard/Default.cshtml b/src/Sections/Humans.Events/Views/Shared/Components/EventsCard/Default.cshtml index bfe65799ed..d5233b94f1 100644 --- a/src/Sections/Humans.Events/Views/Shared/Components/EventsCard/Default.cshtml +++ b/src/Sections/Humans.Events/Views/Shared/Components/EventsCard/Default.cshtml @@ -12,7 +12,7 @@
- Events + @Localizer["Events_CardTitle"]
@foreach (var row in Model.Rows) @@ -24,7 +24,7 @@ @row.CategoryName @if (row.IsRecurring) { - + } From eeea18d340e2c0713ba0b14ad453a79392b7b139 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:43:33 +0200 Subject: [PATCH 13/29] Persist explicit shift offset zeroes --- docs/architecture/debt-ledger.yml | 4 ---- .../Data/Configurations/EventSettingsConfiguration.cs | 8 ++++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/architecture/debt-ledger.yml b/docs/architecture/debt-ledger.yml index 4f48205bcc..59f9975e99 100644 --- a/docs/architecture/debt-ledger.yml +++ b/docs/architecture/debt-ledger.yml @@ -395,10 +395,6 @@ inbox: # sweep scope entirely); PasswordGenerator CSPRNG (explicitly deferred by # Peter, 2026-04-24); BudgetRepository ResponsibleTeam .Include (covered by # grandfathered-hum0024-nav-strip). - - added: 2026-08-11 - id: CENTRAL-14 - what: "EventSettingsConfiguration.cs:44-47 declares HasDefaultValue(-25/-16/-9/-4) on four int offsets without HasSentinel — EF's int sentinel stays 0, so explicitly assigning 0 to any of these offsets is skipped and the DB default written instead (int-flavoured bool-sentinel trap). Pre-existing; surfaced by the EF migration review of the Shifts peel (peel 13, nobodies-collective/Humans#858)" - review: light - added: 2026-08-07 id: CENTRAL-12 what: "Shifts section's own Web layer (ShiftAdminController, ShiftsController, VolunteerTrackingController, ShiftViewModels.cs, ShiftAdminPageBuilder, ShiftBrowsePageBuilder, ShiftDashboardPageBuilder, ShiftBrowseMapper, ShiftFilterResolver, ShiftSignupBucketer, ShiftVolunteerSearchBuilder, DevelopmentDashboardSeeder, ShiftSignupsViewComponent) carries the EF EventSettings entity as a view-model/builder field on read-only display paths — should be BurnSettingsInfo per nobodies-collective/Humans#809's acceptance criteria. Distinct from the write/edit path (EventSettingsFormMapper), which legitimately keeps the entity. ~10 interconnected files forming one section's own display pipeline — warrants its own dedicated section-scoped PR rather than folding into #809's cross-section batch (found while working nobodies-collective/Humans#809)" diff --git a/src/Sections/Humans.Shifts/Data/Configurations/EventSettingsConfiguration.cs b/src/Sections/Humans.Shifts/Data/Configurations/EventSettingsConfiguration.cs index f179af4bf7..1b471a7667 100644 --- a/src/Sections/Humans.Shifts/Data/Configurations/EventSettingsConfiguration.cs +++ b/src/Sections/Humans.Shifts/Data/Configurations/EventSettingsConfiguration.cs @@ -41,10 +41,10 @@ public void Configure(EntityTypeBuilder builder) // Build sub-period boundaries — defaults match the org convention so existing // EventSettings rows backfill with sensible values when the migration adds the // columns. Coordinators can adjust per event via /Admin (admin form). - builder.Property(e => e.FirstCrewStartOffset).HasDefaultValue(-25); - builder.Property(e => e.SetupWeekStartOffset).HasDefaultValue(-16); - builder.Property(e => e.PreEventWeekStartOffset).HasDefaultValue(-9); - builder.Property(e => e.FinishingWeekendStartOffset).HasDefaultValue(-4); + builder.Property(e => e.FirstCrewStartOffset).HasDefaultValue(-25).HasSentinel(-25); + builder.Property(e => e.SetupWeekStartOffset).HasDefaultValue(-16).HasSentinel(-16); + builder.Property(e => e.PreEventWeekStartOffset).HasDefaultValue(-9).HasSentinel(-9); + builder.Property(e => e.FinishingWeekendStartOffset).HasDefaultValue(-4).HasSentinel(-4); builder.HasIndex(e => e.IsActive); } From 524f56c2f85f51b7dea3266de2785b7565602397 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:46:46 +0200 Subject: [PATCH 14/29] Move camp role definition sorting out of repository --- .../Humans.Camps/Data/CampRepository.Roles.cs | 2 +- .../Humans.Camps/Services/CampRoleService.cs | 12 ++++++++---- .../Baselines/DisplaySortInControllers.baseline.txt | 2 -- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs b/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs index 082449ef4e..a663cdb629 100644 --- a/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs +++ b/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs @@ -11,7 +11,7 @@ public async Task> ListDefinitionsAsync(bool i var query = ctx.CampRoleDefinitions.AsNoTracking().AsQueryable(); if (!includeDeactivated) query = query.Where(d => d.DeactivatedAt == null); - return await query.OrderBy(d => d.SortOrder).ThenBy(d => d.Name).ToListAsync(ct); + return await query.ToListAsync(ct); } public async Task GetDefinitionByIdAsync(Guid id, CancellationToken ct = default) diff --git a/src/Sections/Humans.Camps/Services/CampRoleService.cs b/src/Sections/Humans.Camps/Services/CampRoleService.cs index b7cdb5131a..5fa5575815 100644 --- a/src/Sections/Humans.Camps/Services/CampRoleService.cs +++ b/src/Sections/Humans.Camps/Services/CampRoleService.cs @@ -23,7 +23,7 @@ internal sealed class CampRoleService( public async Task> ListDefinitionsAsync(bool includeDeactivated, CancellationToken ct = default) { - var definitions = await repo.ListDefinitionsAsync(includeDeactivated, ct); + var definitions = OrderDefinitions(await repo.ListDefinitionsAsync(includeDeactivated, ct)); return definitions.Select(CreateCampRoleDefinitionInfo).ToList(); } @@ -222,7 +222,7 @@ await auditLog.LogAsync( public async Task BuildPanelAsync(Guid campSeasonId, CancellationToken ct = default) { - var definitions = await repo.ListDefinitionsAsync(includeDeactivated: false, ct); + var definitions = OrderDefinitions(await repo.ListDefinitionsAsync(includeDeactivated: false, ct)); var assignments = await repo.GetAssignmentsForSeasonAsync(campSeasonId, ct); var memberUserIds = assignments.Select(a => a.CampMember.UserId).Distinct().ToList(); @@ -377,8 +377,8 @@ await auditLog.LogAsync( public async Task>> GetDirectoryRoleSummariesAsync(int year, CancellationToken ct = default) { - var definitions = await repo.ListDefinitionsAsync(includeDeactivated: false, ct); - if (definitions.Count == 0) + var definitions = OrderDefinitions(await repo.ListDefinitionsAsync(includeDeactivated: false, ct)); + if (!definitions.Any()) return new Dictionary>(); // Same shape as GetComplianceReportAsync but over all active definitions, @@ -657,6 +657,10 @@ async Task> ICampRoleSeeding.ListDefin return definitions.Select(d => new CampRoleDefinitionSeedInfo(d.Id, d.Slug, d.Name)).ToList(); } + private static IOrderedEnumerable OrderDefinitions( + IEnumerable definitions) => + definitions.OrderBy(d => d.SortOrder).ThenBy(d => d.Name, StringComparer.OrdinalIgnoreCase); + /// async Task ICampRoleSeeding.CreateDefinitionForSeedAsync( string name, string slug, string? description, int slotCount, int minimumRequired, diff --git a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt index a19f551a33..ca0c300197 100644 --- a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt +++ b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt @@ -6,8 +6,6 @@ # When violations are FIXED, remove the corresponding line from this file. # When the test reports new violations, fix the code — do not add lines to silence it. -src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:OrderBy#1 -src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:ThenBy#1 src/Sections/Humans.Teams/Data/TeamRepository.cs:OrderBy#1 src/Sections/Humans.Teams/Data/TeamRepository.cs:ThenBy#1 src/Sections/Humans.Teams/Data/TeamRepository.cs:ThenBy#2 From 60d7c4401a48629b646093c0ab79a3b8deaf2278 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:50:27 +0200 Subject: [PATCH 15/29] Localize rideshare notifications --- .../RideshareResource.ca.resx | 10 ++++ .../RideshareResource.de.resx | 10 ++++ .../RideshareResource.es.resx | 10 ++++ .../RideshareResource.fr.resx | 10 ++++ .../RideshareResource.it.resx | 10 ++++ .../Humans.Rideshare/RideshareResource.resx | 10 ++++ .../Services/RideshareService.cs | 50 ++++++++++++------- 7 files changed, 92 insertions(+), 18 deletions(-) diff --git a/src/Sections/Humans.Rideshare/RideshareResource.ca.resx b/src/Sections/Humans.Rideshare/RideshareResource.ca.resx index 4aec6a277e..a3cdf4a66c 100644 --- a/src/Sections/Humans.Rideshare/RideshareResource.ca.resx +++ b/src/Sections/Humans.Rideshare/RideshareResource.ca.resx @@ -187,4 +187,14 @@ Ofereix almenys una plaça. Una sol·licitud és per a almenys una persona. Aquest viatge no va en aquesta direcció el dia que ho necessiten. + {0} està interessat en el teu viatge + {0} et pot portar + {0} · {1} · {2} + 1 plaça + {0} places + Hi ets: viatges amb {0} + Actualització del viatge + {0} no ha pogut oferir una plaça aquesta vegada. + {0} ha anat amb un altre viatge aquesta vegada. + Obre viatges compartits diff --git a/src/Sections/Humans.Rideshare/RideshareResource.de.resx b/src/Sections/Humans.Rideshare/RideshareResource.de.resx index 1e8847eb87..7a11d72664 100644 --- a/src/Sections/Humans.Rideshare/RideshareResource.de.resx +++ b/src/Sections/Humans.Rideshare/RideshareResource.de.resx @@ -187,4 +187,14 @@ Biete mindestens einen Platz an. Eine Anfrage gilt für mindestens eine Person. Diese Fahrt geht an dem Tag nicht in diese Richtung. + {0} interessiert sich für deine Fahrt + {0} kann dich mitnehmen + {0} · {1} · {2} + 1 Platz + {0} Plätze + Du bist dabei: Fahrt mit {0} + Fahrt-Update + {0} konnte diesmal keinen Platz anbieten. + {0} ist diesmal mit einer anderen Fahrt gefahren. + Mitfahrgelegenheiten öffnen diff --git a/src/Sections/Humans.Rideshare/RideshareResource.es.resx b/src/Sections/Humans.Rideshare/RideshareResource.es.resx index 01e77f3958..058e81cd95 100644 --- a/src/Sections/Humans.Rideshare/RideshareResource.es.resx +++ b/src/Sections/Humans.Rideshare/RideshareResource.es.resx @@ -187,4 +187,14 @@ Ofrece al menos una plaza. Una solicitud es para al menos una persona. Ese viaje no va en esa dirección el día que lo necesitan. + {0} está interesado en tu viaje + {0} puede llevarte + {0} · {1} · {2} + 1 plaza + {0} plazas + Estás dentro: viajas con {0} + Actualización del viaje + {0} no pudo ofrecer una plaza esta vez. + {0} viajó con otro coche esta vez. + Abrir viajes compartidos diff --git a/src/Sections/Humans.Rideshare/RideshareResource.fr.resx b/src/Sections/Humans.Rideshare/RideshareResource.fr.resx index 7b0c54e289..e4243c4009 100644 --- a/src/Sections/Humans.Rideshare/RideshareResource.fr.resx +++ b/src/Sections/Humans.Rideshare/RideshareResource.fr.resx @@ -187,4 +187,14 @@ Propose au moins une place. Une demande concerne au moins une personne. Ce trajet ne va pas dans cette direction le jour où ils en ont besoin. + {0} est intéressé par votre trajet + {0} peut vous emmener + {0} · {1} · {2} + 1 place + {0} places + Vous êtes inscrit : trajet avec {0} + Mise à jour du trajet + {0} n'a pas pu proposer de place cette fois-ci. + {0} a choisi un autre trajet cette fois-ci. + Ouvrir le covoiturage diff --git a/src/Sections/Humans.Rideshare/RideshareResource.it.resx b/src/Sections/Humans.Rideshare/RideshareResource.it.resx index 5b4d5a60fa..bc83ebd9cc 100644 --- a/src/Sections/Humans.Rideshare/RideshareResource.it.resx +++ b/src/Sections/Humans.Rideshare/RideshareResource.it.resx @@ -187,4 +187,14 @@ Offri almeno un posto. Una richiesta è per almeno una persona. Quel passaggio non va in quella direzione il giorno in cui serve. + {0} è interessato al tuo viaggio + {0} può portarti + {0} · {1} · {2} + 1 posto + {0} posti + Sei dentro: viaggio con {0} + Aggiornamento viaggio + {0} non ha potuto offrire un posto questa volta. + {0} ha scelto un altro viaggio questa volta. + Apri viaggi condivisi diff --git a/src/Sections/Humans.Rideshare/RideshareResource.resx b/src/Sections/Humans.Rideshare/RideshareResource.resx index 7dab3aadf0..8149c54169 100644 --- a/src/Sections/Humans.Rideshare/RideshareResource.resx +++ b/src/Sections/Humans.Rideshare/RideshareResource.resx @@ -187,4 +187,14 @@ Offer at least one seat. A request is for at least one person. That ride doesn't go that way on the day they need it. + {0} is interested in your ride + {0} can take you + {0} · {1} · {2} + 1 seat + {0} seats + You're in: ride with {0} + Ride update + {0} wasn't able to offer a spot this time. + {0} went with another ride this time. + Open Rideshare diff --git a/src/Sections/Humans.Rideshare/Services/RideshareService.cs b/src/Sections/Humans.Rideshare/Services/RideshareService.cs index d5ff97ac48..5e0028b690 100644 --- a/src/Sections/Humans.Rideshare/Services/RideshareService.cs +++ b/src/Sections/Humans.Rideshare/Services/RideshareService.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Resources; using System.Text.Json; using Humans.AuditLog.Contracts; using Humans.Base.Extensions; @@ -35,6 +36,7 @@ internal sealed class RideshareService( private const string MineUrl = "/Rideshare/Mine"; private const string MineLabel = "Open Rideshare"; private const string FallbackName = "A human"; + private static readonly ResourceManager NoticeResources = new(typeof(RideshareResource)); // camelCase + case-insensitive: WaypointsJson is {label, latitude, longitude}. private static readonly JsonSerializerOptions WaypointJsonOptions = new(JsonSerializerDefaults.Web); @@ -265,14 +267,17 @@ public async Task ExpressInterestAsync( var name = await DisplayNameAsync(fromUserId, ct); var recipient = request?.UserId ?? trip.UserId; - var (title, place, date) = request is null - ? ($"{name} is interested in your ride", trip.MemberPlaceLabel, trip.DepartureDate) - : ($"{name} can take you", request.PickupPlaceLabel, request.DesiredDate); - var body = $"{place} · {date.ToWeekdayDayMonth()} · {SeatsText(seats)}"; - if (interest.Message is not null) - body += $"\n\"{interest.Message}\""; - - await NotifyAsync(NotificationSource.RideshareInterestReceived, NotificationClass.Actionable, recipient, title, body, ct); + var (titleKey, place, date) = request is null + ? ("Rideshare_NoticeInterestRide", trip.MemberPlaceLabel, trip.DepartureDate) + : ("Rideshare_NoticeCanTakeYou", request.PickupPlaceLabel, request.DesiredDate); + + await NotifyAsync(NotificationSource.RideshareInterestReceived, NotificationClass.Actionable, recipient, culture => + { + var body = Notice(culture, "Rideshare_NoticeTripDetails", place, date.ToWeekdayDayMonth(), SeatsText(seats, culture)); + if (interest.Message is not null) + body += $"\n\"{interest.Message}\""; + return (Notice(culture, titleKey, name), body); + }, ct); return interest.Id; } @@ -295,8 +300,9 @@ public async Task AcceptInterestAsync(Guid interestId, Guid actorUserId, Cancell var name = await DisplayNameAsync(actorUserId, ct); await NotifyAsync( NotificationSource.RideshareInterestAccepted, NotificationClass.Informational, interest.FromUserId, - $"You're in: ride with {name}", - $"{interest.Trip.MemberPlaceLabel} · {interest.Trip.DepartureDate.ToWeekdayDayMonth()} · {SeatsText(interest.Seats)}", + culture => (Notice(culture, "Rideshare_NoticeAccepted", name), + Notice(culture, "Rideshare_NoticeTripDetails", interest.Trip.MemberPlaceLabel, + interest.Trip.DepartureDate.ToWeekdayDayMonth(), SeatsText(interest.Seats, culture))), ct); } @@ -313,13 +319,12 @@ public async Task DeclineInterestAsync(Guid interestId, Guid actorUserId, Cancel // Declines are private: neutral wording, no reason captured or shown. // A rider declining a driver's answer to their pin reads differently from a driver declining a rider. var name = await DisplayNameAsync(actorUserId, ct); - var body = interest.RequestId is null - ? $"{name} wasn't able to offer a spot this time." - : $"{name} went with another ride this time."; await NotifyAsync( NotificationSource.RideshareInterestDeclined, NotificationClass.Informational, interest.FromUserId, - "Ride update", - body, + culture => (Notice(culture, "Rideshare_NoticeUpdate"), + Notice(culture, interest.RequestId is null + ? "Rideshare_NoticeDeclinedOffer" + : "Rideshare_NoticeDeclinedRider", name)), ct); } @@ -614,7 +619,12 @@ private static RideshareDirection Flip(RideshareDirection direction) => private static string? Clean(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); - private static string SeatsText(int seats) => seats == 1 ? "1 seat" : $"{seats} seats"; + private static string SeatsText(int seats, CultureInfo culture) => seats == 1 + ? Notice(culture, "Rideshare_NoticeSeat") + : Notice(culture, "Rideshare_NoticeSeats", seats); + + private static string Notice(CultureInfo culture, string key, params object[] args) => + string.Format(culture, NoticeResources.GetString(key, culture)!, args); private static IReadOnlyList ParseWaypoints(string? json) => string.IsNullOrWhiteSpace(json) @@ -654,13 +664,17 @@ private async Task DisplayNameAsync(Guid userId, CancellationToken ct) // Notifications are best-effort: a failed send never rolls back the interest write. private async Task NotifyAsync( NotificationSource source, NotificationClass notificationClass, Guid recipientUserId, - string title, string body, CancellationToken ct) + Func content, CancellationToken ct) { try { + var language = (await users.GetUserInfoAsync(recipientUserId, ct))?.PreferredLanguage ?? "en"; + var culture = CultureInfo.GetCultureInfo(language); + var (title, body) = content(culture); await notifications.SendAsync( source, notificationClass, NotificationPriority.Normal, title, [recipientUserId], - body: body, actionUrl: MineUrl, actionLabel: MineLabel, cancellationToken: ct); + body: body, actionUrl: MineUrl, + actionLabel: Notice(culture, "Rideshare_NoticeOpen"), cancellationToken: ct); } catch (Exception ex) { From 1f70242a562a4249b19638bbefb1f3ef1c4fffda Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:51:39 +0200 Subject: [PATCH 16/29] Limit Codex command output --- .codex/TECH_DEBT_QUEUE.md | 6 +++--- .codex/bug-hunt-prompt.md | 2 +- .codex/skills/humans-bug-hunt/SKILL.md | 4 ++-- .codex/skills/humans-refactor/SKILL.md | 2 +- .codex/skills/humans-tech-debt/SKILL.md | 4 ++-- .codex/tech-debt-prompt.md | 4 ++-- docs/architecture/debt-ledger.yml | 4 ---- 7 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.codex/TECH_DEBT_QUEUE.md b/.codex/TECH_DEBT_QUEUE.md index 0b4d4ac1b2..0a1105b739 100644 --- a/.codex/TECH_DEBT_QUEUE.md +++ b/.codex/TECH_DEBT_QUEUE.md @@ -33,7 +33,7 @@ violation, never edit a baseline the code still triggers. `src/Sections/Humans.
/Docs/
.md` + `Docs/data-access.md`. Debt is whatever diverges from that model; find it by comparing a section against the model, not by consuming a frozen list. -- **Surface / interconnectivity baseline:** `dotnet build Humans.slnx -v quiet`, then +- **Surface / interconnectivity baseline:** `dotnet build Humans.slnx -v quiet -clp:ErrorsOnly`, then `reforge surface-score --all --top-symbols 200 --format Json` (score a **built** solution — unbuilt under-reports ~4%). Rank sections by the Section Refactor History table in `docs/architecture/maintenance-log.md`. The score is a detector, not an @@ -71,8 +71,8 @@ violation, never edit a baseline the code still triggers. *Current state* below **by rewriting it**. 2. Pick one item by the priority order. Write a one-sentence architecture thesis; if the thesis is "a number goes down", pick something else. -3. Make the smallest real refactor. Targeted section tests + `dotnet build Humans.slnx -v quiet` - per change; full `dotnet test Humans.slnx -v quiet` before any push. +3. Make the smallest real refactor. Targeted section tests + `dotnet build Humans.slnx -v quiet -clp:ErrorsOnly` + per change; full `dotnet test Humans.slnx -v quiet -clp:ErrorsOnly` before any push. 4. One coherent improvement per commit; push the branch; open/refresh the PR (`memory/process/always-open-a-pr.md`). 5. When stopping: rewrite *Current state* (including *Needs Peter*), leave the diff --git a/.codex/bug-hunt-prompt.md b/.codex/bug-hunt-prompt.md index 88b5be69b8..f719ca7469 100644 --- a/.codex/bug-hunt-prompt.md +++ b/.codex/bug-hunt-prompt.md @@ -76,7 +76,7 @@ Do not modify: ## Build Command ``` -dotnet build Humans.slnx -v q && dotnet test Humans.slnx -v q --filter "FullyQualifiedName~Application" +dotnet build Humans.slnx -v q -clp:ErrorsOnly && dotnet test Humans.slnx -v q -clp:ErrorsOnly --filter "FullyQualifiedName~Application" ``` ## Phase 1: Razor Rendering & HTML Structure *(highest frequency — 15+ historical fixes)* diff --git a/.codex/skills/humans-bug-hunt/SKILL.md b/.codex/skills/humans-bug-hunt/SKILL.md index ae0b578088..3b7139e4e8 100644 --- a/.codex/skills/humans-bug-hunt/SKILL.md +++ b/.codex/skills/humans-bug-hunt/SKILL.md @@ -27,8 +27,8 @@ Run recurring autonomous bug-hunt passes in this repository. 2. Search for one high-confidence bug at a time, using repo patterns rather than a fixed checklist. 3. Implement the smallest defensible fix. 4. Add or extend tests when practical. -5. Run targeted verification, plus `dotnet build Humans.slnx --disable-build-servers -v q`. -6. Periodically run `dotnet test Humans.slnx --no-build --disable-build-servers -v q --filter "FullyQualifiedName~Application"`. +5. Run targeted verification, plus `dotnet build Humans.slnx --disable-build-servers -v q -clp:ErrorsOnly`. +6. Periodically run `dotnet test Humans.slnx --no-build --disable-build-servers -v q -clp:ErrorsOnly --filter "FullyQualifiedName~Application"`. 7. Commit each fix separately and push the branch after verified progress. 8. Continue until remaining ideas are speculative or require forbidden areas. diff --git a/.codex/skills/humans-refactor/SKILL.md b/.codex/skills/humans-refactor/SKILL.md index 26a0b4a3f6..124fd6ffc2 100644 --- a/.codex/skills/humans-refactor/SKILL.md +++ b/.codex/skills/humans-refactor/SKILL.md @@ -161,7 +161,7 @@ Then repeat until stasis: 3. Pick the highest-leverage cohesive improvement, not just the highest scoring rule. 4. Write a candidate thesis in the run notes: what concept will be deleted, which responsibility moves to its rightful owner, or which duplicated/cross-section path disappears. If the thesis is just "the score drops", reject the candidate before editing. 5. Make the change. -6. Run targeted tests and `dotnet build Humans.slnx --disable-build-servers -v q`. +6. Run targeted tests and `dotnet build Humans.slnx --disable-build-servers -v q -clp:ErrorsOnly`. 7. Run Reforge after the change. 8. Run the score-blind architecture-review gate. 9. If accepted, commit and push. If rework/reject, improve or abandon before committing. diff --git a/.codex/skills/humans-tech-debt/SKILL.md b/.codex/skills/humans-tech-debt/SKILL.md index 37bf0fe078..85b375d457 100644 --- a/.codex/skills/humans-tech-debt/SKILL.md +++ b/.codex/skills/humans-tech-debt/SKILL.md @@ -30,9 +30,9 @@ Run recurring autonomous tech-debt reduction passes in this repository. 3. Before editing, write a one-sentence architecture thesis: what concept will be deleted, what responsibility will move to its rightful owner, or what duplication/coupling will disappear. If the thesis is "the score drops", abandon the candidate. 4. Make the smallest coherent improvement that reduces divergence, duplication, misplaced responsibility, or durable public surface. 5. Add or extend tests when practical. -6. Run targeted verification, plus `dotnet build Humans.slnx --disable-build-servers -v q`. +6. Run targeted verification, plus `dotnet build Humans.slnx --disable-build-servers -v q -clp:ErrorsOnly`. 7. Run a score-blind second pass before commit. Review only the diff, the architecture thesis, and verification. Reject the change if it would not be worth keeping without metric movement. -8. Run the full `dotnet test Humans.slnx --no-build --disable-build-servers -v quiet` gate before any push. +8. Run the full `dotnet test Humans.slnx --no-build --disable-build-servers -v quiet -clp:ErrorsOnly` gate before any push. 9. Commit each accepted improvement separately and push the branch after verified progress. 10. Continue until remaining ideas are low-value, speculative, blocked by forbidden areas, or only reducible through metric-gaming changes. diff --git a/.codex/tech-debt-prompt.md b/.codex/tech-debt-prompt.md index 01a492da87..e59a25d129 100644 --- a/.codex/tech-debt-prompt.md +++ b/.codex/tech-debt-prompt.md @@ -32,7 +32,7 @@ file's *Current state* instead of restarting discovery. `docs/sections/SECTION-TEMPLATE.md`, `docs/architecture/design-rules.md`, each section's `Docs/
.md` + `Docs/data-access.md`. Divergence from the model is the debt. -- Surface / coupling baseline: `dotnet build Humans.slnx -v quiet`, then +- Surface / coupling baseline: `dotnet build Humans.slnx -v quiet -clp:ErrorsOnly`, then `reforge surface-score --all` (built solution only). Reduce it through architecturally-real deletions; the score is a detector, never the objective. @@ -46,7 +46,7 @@ file's *Current state* instead of restarting discovery. `local/tech-debt-runs//` either way. - One coherent improvement per commit, each with a one-sentence architecture thesis that stands without score movement. Targeted section tests + build per change; - full `dotnet test Humans.slnx -v quiet` before any push. `-v quiet` always. + full `dotnet test Humans.slnx -v quiet -clp:ErrorsOnly` before any push. `-v quiet` always. - Honor every boundary in the queue file's *Boundaries* section — especially: debt only (never feature follow-ups, even fully-specced ones), no authorization/privacy shape changes, no reverting documented test-infrastructure decisions, and new public surface goes to *Needs Peter* instead of into the code. diff --git a/docs/architecture/debt-ledger.yml b/docs/architecture/debt-ledger.yml index 59f9975e99..67c0c71875 100644 --- a/docs/architecture/debt-ledger.yml +++ b/docs/architecture/debt-ledger.yml @@ -579,10 +579,6 @@ inbox: id: CENTRAL-59 what: "src/Sections/Humans.Users.Contracts/IUserInfoInvalidator.cs:24 deliberately omits the IInvalidator marker (its own comment says so, debt nobodies-collective/Humans#805), so HUM0028 cannot see it and it needs no [Grandfathered] marker — one invalidator dodging the ratchet by being invisible to it. Marking it (or recording the exemption where the analyzer reads it) is small and separable from the big invalidator rework the HUM0028 theme is parked on. Found by the 2026-09-20 accepted-smell audit." review: light - - added: 2026-09-20 - id: CENTRAL-60 - what: "The pre-existing Codex prompt/skill files run dotnet with `-v quiet`/`-v q` but without the `-clp:ErrorsOnly` that memory/process/dotnet-verbosity-quiet.md requires (.codex/tech-debt-prompt.md, .codex/bug-hunt-prompt.md, .codex/TECH_DEBT_QUEUE.md, .codex/skills/humans-{tech-debt,refactor,bug-hunt}/SKILL.md). Without it the obsolete-warning wall floods the context of every Codex run that follows them, which costs most on a time-budgeted unattended run. Mechanical fix, no judgement needed. Found 2026-09-20 while adding the flag to the nightly debt runner in peterdrier/Humans#1759, which deliberately did not widen to these files." - review: light - added: 2026-09-20 id: CENTRAL-61 what: "Two pre-existing Codex runner scripts use `git -C `, which memory/process/never-use-git-dash-c.md marks a HARD RULE with no exceptions: .codex/run-weekly-bug-hunt.sh and .codex/cleanup-merged-bug-hunt-worktrees.sh. Fix is mechanical — run each group in a `(cd \"$dir\" && git ...)` subshell, as .codex/cron/run-daily-debt.sh now does. Worth noting the atom's stated rationale (a wrong shell folder; defeating Bash allowlist prefixes) is about agent-issued commands, so Peter may want to say whether the rule binds committed scripts at all rather than have it re-found every audit. Found 2026-09-20 while fixing the same violation in the nightly debt runner in peterdrier/Humans#1759." From 59c6bdae82987179cc70a174eb5bc232388e2593 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:55:28 +0200 Subject: [PATCH 17/29] Localize profile communication feedback --- src/Sections/Humans.Users/Controllers/ProfileController.cs | 2 +- .../Humans.Users/Controllers/ProfileEmailsController.cs | 2 +- .../Humans.Users/Controllers/ProfileViewController.cs | 4 ++-- src/Sections/Humans.Users/UsersResource.ca.resx | 3 +++ src/Sections/Humans.Users/UsersResource.de.resx | 3 +++ src/Sections/Humans.Users/UsersResource.es.resx | 3 +++ src/Sections/Humans.Users/UsersResource.fr.resx | 3 +++ src/Sections/Humans.Users/UsersResource.it.resx | 3 +++ src/Sections/Humans.Users/UsersResource.resx | 3 +++ 9 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Sections/Humans.Users/Controllers/ProfileController.cs b/src/Sections/Humans.Users/Controllers/ProfileController.cs index 2df36a8136..0359cd15c6 100644 --- a/src/Sections/Humans.Users/Controllers/ProfileController.cs +++ b/src/Sections/Humans.Users/Controllers/ProfileController.cs @@ -841,7 +841,7 @@ public async Task CommunicationPreferences() catch (Exception ex) { logger.LogError(ex, "Failed to load communication preferences"); - SetError("Failed to load communication preferences."); + SetError(localizer["Profile_CommunicationPreferencesLoadFailed"].Value); return RedirectToAction(nameof(Me)); } } diff --git a/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs b/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs index bd12cf52e5..080e969e13 100644 --- a/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs +++ b/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs @@ -135,7 +135,7 @@ private void SetAddedEmailFlash(string email, bool isConflict) { if (isConflict) { - SetInfo("This email is linked to another account. Verifying it will request an account merge. Check your inbox for the verification link."); + SetInfo(localizer["Profile_EmailLinkedToAnotherAccount"].Value); return; } diff --git a/src/Sections/Humans.Users/Controllers/ProfileViewController.cs b/src/Sections/Humans.Users/Controllers/ProfileViewController.cs index 303a4d1252..e4a1b8009e 100644 --- a/src/Sections/Humans.Users/Controllers/ProfileViewController.cs +++ b/src/Sections/Humans.Users/Controllers/ProfileViewController.cs @@ -299,7 +299,7 @@ public async Task SendMessage(Guid id, Guid? teamId, Cancellation if (!await commPrefService.AcceptsFacilitatedMessagesAsync(id, ct)) { - SetError("This human has opted out of receiving messages."); + SetError(localizer["Profile_MessageOptedOut"].Value); return RedirectToAction(nameof(ViewProfile), new { id }); } @@ -344,7 +344,7 @@ public async Task SendMessage(Guid id, SendMessageViewModel model if (!await commPrefService.AcceptsFacilitatedMessagesAsync(id, ct)) { - SetError("This human has opted out of receiving messages."); + SetError(localizer["Profile_MessageOptedOut"].Value); return RedirectToAction(nameof(ViewProfile), new { id }); } diff --git a/src/Sections/Humans.Users/UsersResource.ca.resx b/src/Sections/Humans.Users/UsersResource.ca.resx index 37f72f92ea..108a3f3b7f 100644 --- a/src/Sections/Humans.Users/UsersResource.ca.resx +++ b/src/Sections/Humans.Users/UsersResource.ca.resx @@ -1154,4 +1154,7 @@ <p>Una salutació,<br/>L’equip de Humans</p> {0} = user name + Aquesta persona ha decidit no rebre missatges. + No s'han pogut carregar les preferències de comunicació. + Aquest correu està vinculat a un altre compte. En verificar-lo se sol·licitarà una fusió de comptes. Consulta la safata d'entrada per a l'enllaç de verificació. diff --git a/src/Sections/Humans.Users/UsersResource.de.resx b/src/Sections/Humans.Users/UsersResource.de.resx index 681bce588c..90e6807fab 100644 --- a/src/Sections/Humans.Users/UsersResource.de.resx +++ b/src/Sections/Humans.Users/UsersResource.de.resx @@ -1154,4 +1154,7 @@ <p>Alles Gute,<br/>Das Humans-Team</p> {0} = user name + Diese Person hat den Empfang von Nachrichten abgelehnt. + Kommunikationseinstellungen konnten nicht geladen werden. + Diese E-Mail ist mit einem anderen Konto verknüpft. Die Verifizierung fordert eine Kontozusammenführung an. Prüfe deinen Posteingang auf den Bestätigungslink. diff --git a/src/Sections/Humans.Users/UsersResource.es.resx b/src/Sections/Humans.Users/UsersResource.es.resx index 7a1ec063ff..f7505c5722 100644 --- a/src/Sections/Humans.Users/UsersResource.es.resx +++ b/src/Sections/Humans.Users/UsersResource.es.resx @@ -1154,4 +1154,7 @@ <p>Un saludo,<br/>El equipo de Humans</p> {0} = user name + Esta persona ha decidido no recibir mensajes. + No se pudieron cargar las preferencias de comunicación. + Este correo está vinculado a otra cuenta. Al verificarlo se solicitará una fusión de cuentas. Consulta tu bandeja de entrada para ver el enlace de verificación. diff --git a/src/Sections/Humans.Users/UsersResource.fr.resx b/src/Sections/Humans.Users/UsersResource.fr.resx index 75bf86831d..335b26eee7 100644 --- a/src/Sections/Humans.Users/UsersResource.fr.resx +++ b/src/Sections/Humans.Users/UsersResource.fr.resx @@ -1154,4 +1154,7 @@ <p>Cordialement,<br/>L'équipe Humans</p> {0} = user name + Cette personne a choisi de ne pas recevoir de messages. + Impossible de charger les préférences de communication. + Cette adresse e-mail est liée à un autre compte. Sa vérification demandera une fusion de comptes. Consultez votre boîte de réception pour le lien de vérification. diff --git a/src/Sections/Humans.Users/UsersResource.it.resx b/src/Sections/Humans.Users/UsersResource.it.resx index b594ff8bce..a950050a47 100644 --- a/src/Sections/Humans.Users/UsersResource.it.resx +++ b/src/Sections/Humans.Users/UsersResource.it.resx @@ -1154,4 +1154,7 @@ <p>Cordiali saluti,<br/>Il team di Humans</p> {0} = user name + Questa persona ha scelto di non ricevere messaggi. + Impossibile caricare le preferenze di comunicazione. + Questa email è collegata a un altro account. La verifica richiederà un'unione di account. Controlla la posta in arrivo per il link di verifica. diff --git a/src/Sections/Humans.Users/UsersResource.resx b/src/Sections/Humans.Users/UsersResource.resx index 4e5d79ecc6..4348923184 100644 --- a/src/Sections/Humans.Users/UsersResource.resx +++ b/src/Sections/Humans.Users/UsersResource.resx @@ -549,4 +549,7 @@ <p>As requested, your Humans account has been permanently deleted. All your personal data has been removed from our systems.</p> <p>Thank you for being part of our community. If you ever wish to rejoin, you're welcome to create a new account.</p> <p>Best wishes,<br/>The Humans Team</p>{0} = user name + This human has opted out of receiving messages. + Failed to load communication preferences. + This email is linked to another account. Verifying it will request an account merge. Check your inbox for the verification link. From 0961c356982351ddae8e3bce40afc0d81d6c7878 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 08:58:21 +0200 Subject: [PATCH 18/29] Localize email merge confirmation --- .../Humans.Users/Controllers/ProfileEmailsController.cs | 5 ++++- src/Sections/Humans.Users/UsersResource.ca.resx | 1 + src/Sections/Humans.Users/UsersResource.de.resx | 1 + src/Sections/Humans.Users/UsersResource.es.resx | 1 + src/Sections/Humans.Users/UsersResource.fr.resx | 1 + src/Sections/Humans.Users/UsersResource.it.resx | 1 + src/Sections/Humans.Users/UsersResource.resx | 1 + 7 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs b/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs index 080e969e13..4eb19a9df5 100644 --- a/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs +++ b/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs @@ -179,7 +179,10 @@ private IActionResult VerifyEmailSuccess(Guid userId, VerifyEmailResult result) userId, result.Email); ViewData["Success"] = true; - ViewData["Message"] = $"Email verified. A merge request has been submitted for admin review. The email {result.Email} will be added to your account once approved."; + ViewData["Message"] = string.Format( + CultureInfo.CurrentCulture, + localizer["Profile_EmailVerifiedMergeRequested"].Value, + result.Email); return View("VerifyEmailResult"); } diff --git a/src/Sections/Humans.Users/UsersResource.ca.resx b/src/Sections/Humans.Users/UsersResource.ca.resx index 108a3f3b7f..777ad6adf6 100644 --- a/src/Sections/Humans.Users/UsersResource.ca.resx +++ b/src/Sections/Humans.Users/UsersResource.ca.resx @@ -1157,4 +1157,5 @@ Aquesta persona ha decidit no rebre missatges. No s'han pogut carregar les preferències de comunicació. Aquest correu està vinculat a un altre compte. En verificar-lo se sol·licitarà una fusió de comptes. Consulta la safata d'entrada per a l'enllaç de verificació. + Correu verificat. S'ha enviat una sol·licitud de fusió per a la revisió administrativa. El correu {0} s'afegirà al teu compte quan s'aprovi.{0} = email address diff --git a/src/Sections/Humans.Users/UsersResource.de.resx b/src/Sections/Humans.Users/UsersResource.de.resx index 90e6807fab..db6f2f2788 100644 --- a/src/Sections/Humans.Users/UsersResource.de.resx +++ b/src/Sections/Humans.Users/UsersResource.de.resx @@ -1157,4 +1157,5 @@ Diese Person hat den Empfang von Nachrichten abgelehnt. Kommunikationseinstellungen konnten nicht geladen werden. Diese E-Mail ist mit einem anderen Konto verknüpft. Die Verifizierung fordert eine Kontozusammenführung an. Prüfe deinen Posteingang auf den Bestätigungslink. + E-Mail verifiziert. Eine Zusammenführungsanfrage wurde zur Prüfung durch die Administration eingereicht. Die E-Mail-Adresse {0} wird nach der Genehmigung deinem Konto hinzugefügt.{0} = email address diff --git a/src/Sections/Humans.Users/UsersResource.es.resx b/src/Sections/Humans.Users/UsersResource.es.resx index f7505c5722..0def960963 100644 --- a/src/Sections/Humans.Users/UsersResource.es.resx +++ b/src/Sections/Humans.Users/UsersResource.es.resx @@ -1157,4 +1157,5 @@ Esta persona ha decidido no recibir mensajes. No se pudieron cargar las preferencias de comunicación. Este correo está vinculado a otra cuenta. Al verificarlo se solicitará una fusión de cuentas. Consulta tu bandeja de entrada para ver el enlace de verificación. + Correo verificado. Se ha enviado una solicitud de fusión para revisión administrativa. El correo {0} se añadirá a tu cuenta cuando se apruebe.{0} = email address diff --git a/src/Sections/Humans.Users/UsersResource.fr.resx b/src/Sections/Humans.Users/UsersResource.fr.resx index 335b26eee7..6ee42770fb 100644 --- a/src/Sections/Humans.Users/UsersResource.fr.resx +++ b/src/Sections/Humans.Users/UsersResource.fr.resx @@ -1157,4 +1157,5 @@ Cette personne a choisi de ne pas recevoir de messages. Impossible de charger les préférences de communication. Cette adresse e-mail est liée à un autre compte. Sa vérification demandera une fusion de comptes. Consultez votre boîte de réception pour le lien de vérification. + Adresse e-mail vérifiée. Une demande de fusion a été soumise à l'examen administratif. L'adresse e-mail {0} sera ajoutée à votre compte après approbation.{0} = email address diff --git a/src/Sections/Humans.Users/UsersResource.it.resx b/src/Sections/Humans.Users/UsersResource.it.resx index a950050a47..1d2840a4b1 100644 --- a/src/Sections/Humans.Users/UsersResource.it.resx +++ b/src/Sections/Humans.Users/UsersResource.it.resx @@ -1157,4 +1157,5 @@ Questa persona ha scelto di non ricevere messaggi. Impossibile caricare le preferenze di comunicazione. Questa email è collegata a un altro account. La verifica richiederà un'unione di account. Controlla la posta in arrivo per il link di verifica. + Email verificata. È stata inviata una richiesta di unione per la revisione amministrativa. L'email {0} verrà aggiunta al tuo account una volta approvata.{0} = email address diff --git a/src/Sections/Humans.Users/UsersResource.resx b/src/Sections/Humans.Users/UsersResource.resx index 4348923184..770c9b70ba 100644 --- a/src/Sections/Humans.Users/UsersResource.resx +++ b/src/Sections/Humans.Users/UsersResource.resx @@ -552,4 +552,5 @@ This human has opted out of receiving messages. Failed to load communication preferences. This email is linked to another account. Verifying it will request an account merge. Check your inbox for the verification link. + Email verified. A merge request has been submitted for admin review. The email {0} will be added to your account once approved.{0} = email address From 6acbf496047d237905c074a0fcc99e60517d9081 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 09:04:20 +0200 Subject: [PATCH 19/29] Localize guest account feedback --- .../Controllers/GuestAccountController.cs | 22 +++++++++++------- .../Models/GuestDeletionRequestFlash.cs | 23 ++++++++----------- .../Humans.Users/UsersResource.ca.resx | 4 ++++ .../Humans.Users/UsersResource.de.resx | 4 ++++ .../Humans.Users/UsersResource.es.resx | 4 ++++ .../Humans.Users/UsersResource.fr.resx | 4 ++++ .../Humans.Users/UsersResource.it.resx | 4 ++++ src/Sections/Humans.Users/UsersResource.resx | 4 ++++ .../GuestAccountControllerTests.cs | 17 +++++++++++++- 9 files changed, 64 insertions(+), 22 deletions(-) diff --git a/src/Sections/Humans.Users/Controllers/GuestAccountController.cs b/src/Sections/Humans.Users/Controllers/GuestAccountController.cs index 0376ea3ce1..5d8174a3a0 100644 --- a/src/Sections/Humans.Users/Controllers/GuestAccountController.cs +++ b/src/Sections/Humans.Users/Controllers/GuestAccountController.cs @@ -5,7 +5,9 @@ using Humans.Tickets.Contracts; using Humans.Users.Contracts; using Humans.Users.Models; +using Microsoft.Extensions.Localization; using NodaTime; +using System.Globalization; namespace Humans.Users.Controllers; @@ -23,7 +25,8 @@ internal sealed class GuestAccountController( ITicketServiceRead ticketQueryService, IAccountDeletionService accountDeletionService, IClock clock, - ILogger logger) : HumansControllerBase(userService) + ILogger logger, + IStringLocalizer localizer) : HumansControllerBase(userService) { // WARNING: [AllowAnonymous] — accepts unauthenticated requests with a valid unsubscribe // token (utoken). The token scopes access to THIS page only. Do not add links to other @@ -46,7 +49,7 @@ public async Task CommunicationPreferences(string? utoken) catch (Exception ex) { logger.LogError(ex, "Failed to load communication preferences"); - SetError("Failed to load communication preferences."); + SetError(localizer["Profile_CommunicationPreferencesLoadFailed"].Value); return RedirectToAction("Index", "Guest"); } } @@ -83,6 +86,9 @@ await commPrefService.UpdatePreferenceAsync( private static string GetPreferenceUpdateSource(bool fromToken) => fromToken ? "MagicLink" : "Guest"; + private string FormatDeletionFlash(GuestDeletionRequestFlash flash) => + string.Format(CultureInfo.CurrentCulture, localizer[flash.ResourceKey].Value, flash.EffectiveDeletionDate.ToDate()); + [HttpPost("Guest/RequestDeletion")] [ValidateAntiForgeryToken] public async Task RequestDeletion() @@ -98,18 +104,18 @@ public async Task RequestDeletion() var flash = GuestDeletionRequestFlash.From(result); if (!flash.Success) { - SetError(flash.Message); + SetError(FormatDeletionFlash(flash)); return RedirectToAction("Index", "Guest"); } - SetSuccess(flash.Message); + SetSuccess(FormatDeletionFlash(flash)); return RedirectToAction("Index", "Guest"); } catch (Exception ex) { logger.LogError(ex, "Failed to process deletion request for user {UserId}", user.Id); - SetError("Failed to process deletion request. Please try again."); + SetError(localizer["Guest_DeletionRequestFailed"].Value); return RedirectToAction("Index", "Guest"); } } @@ -126,12 +132,12 @@ public async Task CancelDeletion() if (!result.Success) { SetError(string.Equals(result.ErrorKey, "NoDeletionPending", StringComparison.Ordinal) - ? "No deletion request is pending." - : "Failed to cancel deletion request. Please try again."); + ? localizer["Profile_NoDeletionPending"].Value + : localizer["Guest_CancelDeletionFailed"].Value); return RedirectToAction("Index", "Guest"); } - SetSuccess("Deletion request cancelled."); + SetSuccess(localizer["Profile_DeletionCancelled"].Value); return RedirectToAction("Index", "Guest"); } diff --git a/src/Sections/Humans.Users/Models/GuestDeletionRequestFlash.cs b/src/Sections/Humans.Users/Models/GuestDeletionRequestFlash.cs index 2c13282a3e..e30720b2ff 100644 --- a/src/Sections/Humans.Users/Models/GuestDeletionRequestFlash.cs +++ b/src/Sections/Humans.Users/Models/GuestDeletionRequestFlash.cs @@ -1,26 +1,23 @@ -using Humans.Base.Extensions; - using Humans.Users.Contracts; +using NodaTime; namespace Humans.Users.Models; -internal sealed record GuestDeletionRequestFlash(bool Success, string Message) +internal sealed record GuestDeletionRequestFlash(bool Success, string ResourceKey, Instant? EffectiveDeletionDate) { public static GuestDeletionRequestFlash From(DeletionRequestResult result) { if (!result.Success) - return new(false, ErrorMessageFor(result.ErrorKey)); - - var effective = result.EffectiveDeletionDate.ToDate(); - var message = result.IsHeldForTicket - ? $"Deletion request recorded. Because you have tickets for an upcoming event, your account will be deleted after {effective}." - : $"Deletion request recorded. Your account will be permanently deleted on {effective}."; + return new(false, ErrorResourceKeyFor(result.ErrorKey), null); - return new(true, message); + return new( + true, + result.IsHeldForTicket ? "Guest_DeletionHeldForTicket" : "Guest_DeletionRequested", + result.EffectiveDeletionDate); } - private static string ErrorMessageFor(string? errorKey) => + private static string ErrorResourceKeyFor(string? errorKey) => string.Equals(errorKey, "AlreadyPending", StringComparison.Ordinal) - ? "A deletion request is already pending." - : "Failed to process deletion request. Please try again."; + ? "Profile_DeletionAlreadyPending" + : "Guest_DeletionRequestFailed"; } diff --git a/src/Sections/Humans.Users/UsersResource.ca.resx b/src/Sections/Humans.Users/UsersResource.ca.resx index 777ad6adf6..2d40ffe43e 100644 --- a/src/Sections/Humans.Users/UsersResource.ca.resx +++ b/src/Sections/Humans.Users/UsersResource.ca.resx @@ -1158,4 +1158,8 @@ No s'han pogut carregar les preferències de comunicació. Aquest correu està vinculat a un altre compte. En verificar-lo se sol·licitarà una fusió de comptes. Consulta la safata d'entrada per a l'enllaç de verificació. Correu verificat. S'ha enviat una sol·licitud de fusió per a la revisió administrativa. El correu {0} s'afegirà al teu compte quan s'aprovi.{0} = email address + No s'ha pogut processar la sol·licitud d'eliminació. Torna-ho a provar. + Sol·licitud d'eliminació registrada. El teu compte s'eliminarà definitivament el {0}.{0} = deletion date + Sol·licitud d'eliminació registrada. Com que tens entrades per a un esdeveniment proper, el teu compte s'eliminarà després del {0}.{0} = deletion date + No s'ha pogut cancel·lar la sol·licitud d'eliminació. Torna-ho a provar. diff --git a/src/Sections/Humans.Users/UsersResource.de.resx b/src/Sections/Humans.Users/UsersResource.de.resx index db6f2f2788..213088e2c3 100644 --- a/src/Sections/Humans.Users/UsersResource.de.resx +++ b/src/Sections/Humans.Users/UsersResource.de.resx @@ -1158,4 +1158,8 @@ Kommunikationseinstellungen konnten nicht geladen werden. Diese E-Mail ist mit einem anderen Konto verknüpft. Die Verifizierung fordert eine Kontozusammenführung an. Prüfe deinen Posteingang auf den Bestätigungslink. E-Mail verifiziert. Eine Zusammenführungsanfrage wurde zur Prüfung durch die Administration eingereicht. Die E-Mail-Adresse {0} wird nach der Genehmigung deinem Konto hinzugefügt.{0} = email address + Der Löschantrag konnte nicht verarbeitet werden. Bitte versuche es erneut. + Löschantrag erfasst. Dein Konto wird am {0} endgültig gelöscht.{0} = deletion date + Löschantrag erfasst. Da du Tickets für eine bevorstehende Veranstaltung hast, wird dein Konto nach dem {0} gelöscht.{0} = deletion date + Der Löschantrag konnte nicht storniert werden. Bitte versuche es erneut. diff --git a/src/Sections/Humans.Users/UsersResource.es.resx b/src/Sections/Humans.Users/UsersResource.es.resx index 0def960963..82701b1ca8 100644 --- a/src/Sections/Humans.Users/UsersResource.es.resx +++ b/src/Sections/Humans.Users/UsersResource.es.resx @@ -1158,4 +1158,8 @@ No se pudieron cargar las preferencias de comunicación. Este correo está vinculado a otra cuenta. Al verificarlo se solicitará una fusión de cuentas. Consulta tu bandeja de entrada para ver el enlace de verificación. Correo verificado. Se ha enviado una solicitud de fusión para revisión administrativa. El correo {0} se añadirá a tu cuenta cuando se apruebe.{0} = email address + No se pudo procesar la solicitud de eliminación. Inténtalo de nuevo. + Solicitud de eliminación registrada. Tu cuenta se eliminará permanentemente el {0}.{0} = deletion date + Solicitud de eliminación registrada. Como tienes entradas para un próximo evento, tu cuenta se eliminará después del {0}.{0} = deletion date + No se pudo cancelar la solicitud de eliminación. Inténtalo de nuevo. diff --git a/src/Sections/Humans.Users/UsersResource.fr.resx b/src/Sections/Humans.Users/UsersResource.fr.resx index 6ee42770fb..9d3ba57f88 100644 --- a/src/Sections/Humans.Users/UsersResource.fr.resx +++ b/src/Sections/Humans.Users/UsersResource.fr.resx @@ -1158,4 +1158,8 @@ Impossible de charger les préférences de communication. Cette adresse e-mail est liée à un autre compte. Sa vérification demandera une fusion de comptes. Consultez votre boîte de réception pour le lien de vérification. Adresse e-mail vérifiée. Une demande de fusion a été soumise à l'examen administratif. L'adresse e-mail {0} sera ajoutée à votre compte après approbation.{0} = email address + Impossible de traiter la demande de suppression. Veuillez réessayer. + Demande de suppression enregistrée. Votre compte sera définitivement supprimé le {0}.{0} = deletion date + Demande de suppression enregistrée. Comme vous avez des billets pour un prochain événement, votre compte sera supprimé après le {0}.{0} = deletion date + Impossible d'annuler la demande de suppression. Veuillez réessayer. diff --git a/src/Sections/Humans.Users/UsersResource.it.resx b/src/Sections/Humans.Users/UsersResource.it.resx index 1d2840a4b1..59a4516c19 100644 --- a/src/Sections/Humans.Users/UsersResource.it.resx +++ b/src/Sections/Humans.Users/UsersResource.it.resx @@ -1158,4 +1158,8 @@ Impossibile caricare le preferenze di comunicazione. Questa email è collegata a un altro account. La verifica richiederà un'unione di account. Controlla la posta in arrivo per il link di verifica. Email verificata. È stata inviata una richiesta di unione per la revisione amministrativa. L'email {0} verrà aggiunta al tuo account una volta approvata.{0} = email address + Impossibile elaborare la richiesta di eliminazione. Riprova. + Richiesta di eliminazione registrata. Il tuo account verrà eliminato definitivamente il {0}.{0} = deletion date + Richiesta di eliminazione registrata. Poiché hai biglietti per un prossimo evento, il tuo account verrà eliminato dopo il {0}.{0} = deletion date + Impossibile annullare la richiesta di eliminazione. Riprova. diff --git a/src/Sections/Humans.Users/UsersResource.resx b/src/Sections/Humans.Users/UsersResource.resx index 770c9b70ba..e6e5b5fd10 100644 --- a/src/Sections/Humans.Users/UsersResource.resx +++ b/src/Sections/Humans.Users/UsersResource.resx @@ -553,4 +553,8 @@ Failed to load communication preferences. This email is linked to another account. Verifying it will request an account merge. Check your inbox for the verification link. Email verified. A merge request has been submitted for admin review. The email {0} will be added to your account once approved.{0} = email address + Failed to process deletion request. Please try again. + Deletion request recorded. Your account will be permanently deleted on {0}.{0} = deletion date + Deletion request recorded. Because you have tickets for an upcoming event, your account will be deleted after {0}.{0} = deletion date + Failed to cancel deletion request. Please try again. diff --git a/tests/Humans.Users.Tests/Controllers/GuestAccountControllerTests.cs b/tests/Humans.Users.Tests/Controllers/GuestAccountControllerTests.cs index 00413da913..0e393e247b 100644 --- a/tests/Humans.Users.Tests/Controllers/GuestAccountControllerTests.cs +++ b/tests/Humans.Users.Tests/Controllers/GuestAccountControllerTests.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Localization; using NodaTime; using NSubstitute; using Xunit; @@ -28,9 +29,22 @@ public class GuestAccountControllerTests private readonly ITicketServiceRead _ticketQueryService = Substitute.For(); private readonly IAccountDeletionService _accountDeletionService = Substitute.For(); private readonly IClock _clock = Substitute.For(); + private readonly IStringLocalizer _localizer = Substitute.For>(); private GuestAccountController BuildSut(User user) { + _localizer[Arg.Any()].Returns(call => + { + var key = call.ArgAt(0); + var value = key switch + { + "Profile_DeletionAlreadyPending" => "A deletion request is already pending.", + "Guest_DeletionRequested" => "Deletion request recorded. Your account will be permanently deleted on {0}.", + _ => key, + }; + return new LocalizedString(key, value); + }); + _userService.GetUserInfoAsync(user.Id, Arg.Any()) .Returns(new ValueTask(UserInfoFactory.Create( user, @@ -49,7 +63,8 @@ private GuestAccountController BuildSut(User user) _ticketQueryService, _accountDeletionService, _clock, - NullLogger.Instance); + NullLogger.Instance, + _localizer); var http = new DefaultHttpContext { From 839c07473b070a0f67d17d45b30925eefbba495f Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 09:07:31 +0200 Subject: [PATCH 20/29] Localize shift profile errors --- .../Humans.Shifts/Controllers/ShiftProfileController.cs | 5 +++-- src/Sections/Humans.Shifts/ShiftsResource.ca.resx | 2 ++ src/Sections/Humans.Shifts/ShiftsResource.de.resx | 2 ++ src/Sections/Humans.Shifts/ShiftsResource.es.resx | 2 ++ src/Sections/Humans.Shifts/ShiftsResource.fr.resx | 2 ++ src/Sections/Humans.Shifts/ShiftsResource.it.resx | 2 ++ src/Sections/Humans.Shifts/ShiftsResource.resx | 2 ++ 7 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Sections/Humans.Shifts/Controllers/ShiftProfileController.cs b/src/Sections/Humans.Shifts/Controllers/ShiftProfileController.cs index 082b443144..2daab40e16 100644 --- a/src/Sections/Humans.Shifts/Controllers/ShiftProfileController.cs +++ b/src/Sections/Humans.Shifts/Controllers/ShiftProfileController.cs @@ -25,6 +25,7 @@ internal sealed class ShiftProfileController( // SharedResource: the only string this controller resolves is Profile_Updated, // which belongs to Shell's profile vocabulary. IStringLocalizer localizer, + IStringLocalizer shiftsLocalizer, ILogger logger) : HumansControllerBase(userService) { [HttpGet("Me/ShiftInfo")] @@ -41,7 +42,7 @@ public async Task ShiftInfo() catch (Exception ex) { logger.LogError(ex, "Failed to load shift info for user"); - SetError("Failed to load shift info."); + SetError(shiftsLocalizer["ShiftProfile_LoadFailed"].Value); return RedirectToAction("Me", "Profile"); } } @@ -73,7 +74,7 @@ public async Task ShiftInfo(ShiftInfoViewModel model) catch (Exception ex) { logger.LogError(ex, "Failed to save shift info for user"); - SetError("Failed to save shift info."); + SetError(shiftsLocalizer["ShiftProfile_SaveFailed"].Value); return View(model); } } diff --git a/src/Sections/Humans.Shifts/ShiftsResource.ca.resx b/src/Sections/Humans.Shifts/ShiftsResource.ca.resx index 4522672d40..f49a9201f7 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.ca.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.ca.resx @@ -1194,4 +1194,6 @@ <p>Hola {0},</p><p>Un missatge del coordinador de <strong>{2}</strong>:</p><hr /><p>{3}</p><hr /><p>Per a la teva informació, les teves franges en els propers torns de l'equip:</p>{4}<p>Gràcies,</p>{5} + No s'ha pogut carregar la informació dels torns. + No s'ha pogut desar la informació dels torns. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.de.resx b/src/Sections/Humans.Shifts/ShiftsResource.de.resx index 1472fa5fb8..f511ceb26b 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.de.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.de.resx @@ -1194,4 +1194,6 @@ <p>Liebe/r {0},</p><p>Eine Nachricht vom Koordinator von <strong>{2}</strong>:</p><hr /><p>{3}</p><hr /><p>Zur Info, deine Schichten in den kommenden Dienstplänen dieses Teams:</p>{4}<p>Danke,</p>{5} + Schichtinformationen konnten nicht geladen werden. + Schichtinformationen konnten nicht gespeichert werden. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.es.resx b/src/Sections/Humans.Shifts/ShiftsResource.es.resx index 2a7e7167a6..0d98ce2126 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.es.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.es.resx @@ -1192,4 +1192,6 @@ <p>Hola {0},</p><p>Un mensaje del coordinador de <strong>{2}</strong>:</p><hr /><p>{3}</p><hr /><p>Para tu información, tus franjas en los próximos turnos del equipo:</p>{4}<p>Gracias,</p>{5} + No se pudo cargar la información de turnos. + No se pudo guardar la información de turnos. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.fr.resx b/src/Sections/Humans.Shifts/ShiftsResource.fr.resx index 14274dd046..9b2b7098e8 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.fr.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.fr.resx @@ -1194,4 +1194,6 @@ <p>Bonjour {0},</p><p>Un message du coordinateur de <strong>{2}</strong> :</p><hr /><p>{3}</p><hr /><p>Pour information, vos créneaux dans les prochains roulements de cette équipe :</p>{4}<p>Merci,</p>{5} + Impossible de charger les informations de créneaux. + Impossible d'enregistrer les informations de créneaux. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.it.resx b/src/Sections/Humans.Shifts/ShiftsResource.it.resx index d99b6f9862..bca30e5d57 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.it.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.it.resx @@ -1194,4 +1194,6 @@ <p>Ciao {0},</p><p>Un messaggio dal coordinatore di <strong>{2}</strong>:</p><hr /><p>{3}</p><hr /><p>Per tua informazione, le tue fasce nei prossimi turni del team:</p>{4}<p>Grazie,</p>{5} + Impossibile caricare le informazioni sui turni. + Impossibile salvare le informazioni sui turni. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.resx b/src/Sections/Humans.Shifts/ShiftsResource.resx index d49255a722..d7097251af 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.resx @@ -399,4 +399,6 @@ (no shifts on this rota yet) A message about your shifts with {0}{0} = team name (HTML-encoded) <p>Dear {0},</p><p>A message from the coordinator of <strong>{2}</strong>:</p><hr /><p>{3}</p><hr /><p>FYI, your shifts across this team's upcoming rotas:</p>{4}<p>Thank you,</p>{5}{0}=recipient name, {1}=sender name, {2}=team name, {3}=message body, {4}=per-rota shift groups HTML, {5}=sender contact block HTML + Failed to load shift information. + Failed to save shift information. From 03623fbf497adcdc2df6720db5ebda69dc7ac2c4 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 09:10:08 +0200 Subject: [PATCH 21/29] Localize ticket transfer feedback --- .../Controllers/TicketTransferController.cs | 10 ++++++---- src/Sections/Humans.Tickets/TicketsResource.ca.resx | 3 +++ src/Sections/Humans.Tickets/TicketsResource.de.resx | 3 +++ src/Sections/Humans.Tickets/TicketsResource.es.resx | 3 +++ src/Sections/Humans.Tickets/TicketsResource.fr.resx | 3 +++ src/Sections/Humans.Tickets/TicketsResource.it.resx | 3 +++ src/Sections/Humans.Tickets/TicketsResource.resx | 3 +++ 7 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Sections/Humans.Tickets/Controllers/TicketTransferController.cs b/src/Sections/Humans.Tickets/Controllers/TicketTransferController.cs index 15f6ec49d4..8ce8aea9cd 100644 --- a/src/Sections/Humans.Tickets/Controllers/TicketTransferController.cs +++ b/src/Sections/Humans.Tickets/Controllers/TicketTransferController.cs @@ -4,6 +4,7 @@ using Humans.Tickets.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; using Humans.Tickets.Services.Dtos; using Humans.Users.Contracts; @@ -15,7 +16,8 @@ internal sealed class TicketTransferController( ITicketTransferService service, IEarlyEntryService earlyEntryService, IUserServiceRead userService, - ILogger logger) : HumansControllerBase(userService) + ILogger logger, + IStringLocalizer localizer) : HumansControllerBase(userService) { [HttpGet("")] public async Task Index(CancellationToken ct) @@ -52,7 +54,7 @@ public async Task Confirm(Guid attendeeId, Guid receiverUserId, C HolderEarlyEntry = earlyEntry?.EarliestEntryDate, Confirm = confirm, Error = confirm is null - ? "Couldn't set up that transfer — choose one of your tickets and a valid recipient (not yourself)." + ? localizer["TicketTransfer_InvalidSelection"].Value : null, }); } @@ -68,7 +70,7 @@ public async Task Submit(Guid attendeeId, Guid receiverUserId, st { await service.CreateRequestAsync( new TicketTransferRequestDto(attendeeId, receiverUserId, reason ?? string.Empty), user.Id, ct); - SetSuccess("Transfer requested. Our ticketing team will process it and let you know shortly."); + SetSuccess(localizer["TicketTransfer_RequestSubmitted"].Value); return RedirectToAction("Index", "Home"); } catch (InvalidOperationException ex) @@ -101,7 +103,7 @@ public async Task Cancel(Guid id, CancellationToken ct) try { await service.CancelAsync(id, user.Id, ct); - SetSuccess("Transfer cancelled."); + SetSuccess(localizer["TicketTransfer_Cancelled"].Value); } catch (InvalidOperationException ex) { diff --git a/src/Sections/Humans.Tickets/TicketsResource.ca.resx b/src/Sections/Humans.Tickets/TicketsResource.ca.resx index f7a620acfc..818920a7b7 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.ca.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.ca.resx @@ -65,4 +65,7 @@ 3. Confirma Transfereix una entrada Les teves sol·licituds de transferència + No s'ha pogut preparar la transferència: tria una de les teves entrades i una persona destinatària vàlida que no siguis tu. + Transferència sol·licitada. El nostre equip d'entrades la processarà i t'ho farà saber aviat. + Transferència cancel·lada. diff --git a/src/Sections/Humans.Tickets/TicketsResource.de.resx b/src/Sections/Humans.Tickets/TicketsResource.de.resx index f77222b9e6..b1cb79872a 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.de.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.de.resx @@ -65,4 +65,7 @@ 3. Bestätigen Ticket übertragen Deine Übertragungsanfragen + Die Übertragung konnte nicht vorbereitet werden — wähle eines deiner Tickets und eine gültige andere empfangende Person. + Übertragung angefragt. Unser Ticketing-Team bearbeitet sie und gibt dir in Kürze Bescheid. + Übertragung storniert. diff --git a/src/Sections/Humans.Tickets/TicketsResource.es.resx b/src/Sections/Humans.Tickets/TicketsResource.es.resx index 4968bbbb8b..91f03ea521 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.es.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.es.resx @@ -65,4 +65,7 @@ 3. Confirmar Transferir una entrada Tus solicitudes de transferencia + No se pudo preparar la transferencia: elige una de tus entradas y una persona destinataria válida que no seas tú. + Transferencia solicitada. Nuestro equipo de entradas la procesará y te avisará en breve. + Transferencia cancelada. diff --git a/src/Sections/Humans.Tickets/TicketsResource.fr.resx b/src/Sections/Humans.Tickets/TicketsResource.fr.resx index 22870a4fa1..545c13c017 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.fr.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.fr.resx @@ -65,4 +65,7 @@ 3. Confirmer Transférer un billet Vos demandes de transfert + Impossible de préparer ce transfert : choisissez l'un de vos billets et une personne destinataire valide autre que vous-même. + Transfert demandé. Notre équipe billetterie le traitera et vous tiendra informé sous peu. + Transfert annulé. diff --git a/src/Sections/Humans.Tickets/TicketsResource.it.resx b/src/Sections/Humans.Tickets/TicketsResource.it.resx index 9fe3639e50..158eb319fd 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.it.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.it.resx @@ -65,4 +65,7 @@ 3. Conferma Trasferisci un biglietto Le tue richieste di trasferimento + Impossibile preparare il trasferimento: scegli uno dei tuoi biglietti e una persona destinataria valida diversa da te. + Trasferimento richiesto. Il nostro team biglietti lo elaborerà e ti farà sapere a breve. + Trasferimento annullato. diff --git a/src/Sections/Humans.Tickets/TicketsResource.resx b/src/Sections/Humans.Tickets/TicketsResource.resx index 47b2ee8197..a064c664ee 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.resx @@ -54,4 +54,7 @@ 3. Confirm Transfer a ticket Your transfer requests + Couldn't set up that transfer — choose one of your tickets and a valid recipient other than yourself. + Transfer requested. Our ticketing team will process it and let you know shortly. + Transfer cancelled. From 1a1e0b7196fd6b8e2139883f63dd2527bca71ab3 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 09:13:58 +0200 Subject: [PATCH 22/29] Localize event state feedback --- .../Controllers/EventsController.cs | 30 ++++++++++--------- .../Humans.Events/EventsResource.ca.resx | 3 ++ .../Humans.Events/EventsResource.de.resx | 3 ++ .../Humans.Events/EventsResource.es.resx | 3 ++ .../Humans.Events/EventsResource.fr.resx | 3 ++ .../Humans.Events/EventsResource.it.resx | 3 ++ .../Humans.Events/EventsResource.resx | 3 ++ .../Controllers/EventsControllerTests.cs | 4 ++- 8 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/Sections/Humans.Events/Controllers/EventsController.cs b/src/Sections/Humans.Events/Controllers/EventsController.cs index 255285e653..24037e651b 100644 --- a/src/Sections/Humans.Events/Controllers/EventsController.cs +++ b/src/Sections/Humans.Events/Controllers/EventsController.cs @@ -9,6 +9,7 @@ using Humans.Events.Models; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; using NodaTime; using static Humans.Events.Helpers.EventsLookupHelpers; using static Humans.Events.Helpers.EventsTimeHelpers; @@ -26,7 +27,8 @@ internal sealed class EventsController( ICampServiceRead camps, IAuthorizationService authorizationService, IClock clock, - ILogger logger) : HumansCampControllerBase(users, camps, authorizationService) + ILogger logger, + IStringLocalizer localizer) : HumansCampControllerBase(users, camps, authorizationService) { [HttpGet("MySubmissions")] public async Task MySubmissions() @@ -120,7 +122,7 @@ public async Task Submit() var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -140,7 +142,7 @@ public async Task Create(IndividualEventFormViewModel model) var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -197,14 +199,14 @@ public async Task Edit(Guid eventId) if (!guideEvent.CanBeEditedBySubmitter) { - SetError("This event cannot be edited in its current state."); + SetError(localizer["Events_NotEditable"].Value); return RedirectToAction(nameof(MySubmissions)); } var guideSettings = await guide.GetGuideSettingsAsync(); if (guideSettings == null) { - SetError("Guide settings not configured."); + SetError(localizer["Events_GuideSettingsUnavailable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -246,14 +248,14 @@ public async Task Update(Guid eventId, IndividualEventFormViewMod if (!guideEvent.CanBeEditedBySubmitter) { - SetError("This event cannot be edited in its current state."); + SetError(localizer["Events_NotEditable"].Value); return RedirectToAction(nameof(MySubmissions)); } var guideSettings = await guide.GetGuideSettingsAsync(); if (guideSettings == null) { - SetError("Guide settings not configured."); + SetError(localizer["Events_GuideSettingsUnavailable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -306,7 +308,7 @@ public async Task Withdraw(Guid eventId) if (!guideEvent.CanBeWithdrawnBySubmitter) { - SetError("This event cannot be withdrawn in its current state."); + SetError(localizer["Events_NotWithdrawable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -527,7 +529,7 @@ public async Task BarrioSubmit(string slug) var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -547,7 +549,7 @@ public async Task BarrioCreate(string slug, CampEventFormViewMode var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -601,7 +603,7 @@ public async Task BarrioEdit(string slug, Guid eventId) if (!guideEvent.CanBeEditedBySubmitter) { - SetError("This event cannot be edited in its current state."); + SetError(localizer["Events_NotEditable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -642,7 +644,7 @@ public async Task BarrioUpdate(string slug, Guid eventId, CampEve if (!guideEvent.CanBeEditedBySubmitter) { - SetError("This event cannot be edited in its current state."); + SetError(localizer["Events_NotEditable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -700,7 +702,7 @@ public async Task BarrioWithdraw(string slug, Guid eventId) if (!guideEvent.CanBeWithdrawnBySubmitter) { - SetError("This event cannot be withdrawn in its current state."); + SetError(localizer["Events_NotWithdrawable"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -732,7 +734,7 @@ public async Task BulkUploadImport(string slug, IFormFile? file) var guideSettings = await guide.GetGuideSettingsAsync(); if (!IsSubmissionOpen(guideSettings)) { - SetError("The submission window is not currently open."); + SetError(localizer["Events_SubmissionWindowClosed"].Value); return RedirectToAction(nameof(MySubmissions)); } diff --git a/src/Sections/Humans.Events/EventsResource.ca.resx b/src/Sections/Humans.Events/EventsResource.ca.resx index dcd9351a71..accebf44a6 100644 --- a/src/Sections/Humans.Events/EventsResource.ca.resx +++ b/src/Sections/Humans.Events/EventsResource.ca.resx @@ -120,4 +120,7 @@ 1 = prioritat més alta per a la selecció de la guia impresa. Esdeveniments Esdeveniment recurrent + Aquest esdeveniment no es pot editar en el seu estat actual. + Aquest esdeveniment no es pot retirar en el seu estat actual. + Els ajustos de la guia no estan configurats. diff --git a/src/Sections/Humans.Events/EventsResource.de.resx b/src/Sections/Humans.Events/EventsResource.de.resx index 44cab8ce7b..1638dfa2cb 100644 --- a/src/Sections/Humans.Events/EventsResource.de.resx +++ b/src/Sections/Humans.Events/EventsResource.de.resx @@ -120,4 +120,7 @@ 1 = höchste Priorität für die Auswahl im gedruckten Guide. Veranstaltungen Wiederkehrende Veranstaltung + Diese Veranstaltung kann in ihrem aktuellen Status nicht bearbeitet werden. + Diese Veranstaltung kann in ihrem aktuellen Status nicht zurückgezogen werden. + Die Guide-Einstellungen sind nicht konfiguriert. diff --git a/src/Sections/Humans.Events/EventsResource.es.resx b/src/Sections/Humans.Events/EventsResource.es.resx index 2e9921642f..1e05581c85 100644 --- a/src/Sections/Humans.Events/EventsResource.es.resx +++ b/src/Sections/Humans.Events/EventsResource.es.resx @@ -120,4 +120,7 @@ 1 = prioridad máxima para la selección de la guía impresa. Eventos Evento recurrente + Este evento no se puede editar en su estado actual. + Este evento no se puede retirar en su estado actual. + Los ajustes de la guía no están configurados. diff --git a/src/Sections/Humans.Events/EventsResource.fr.resx b/src/Sections/Humans.Events/EventsResource.fr.resx index 405d2fcda7..459c202dbb 100644 --- a/src/Sections/Humans.Events/EventsResource.fr.resx +++ b/src/Sections/Humans.Events/EventsResource.fr.resx @@ -120,4 +120,7 @@ 1 = priorité la plus élevée pour la sélection du guide imprimé. Événements Événement récurrent + Cet événement ne peut pas être modifié dans son état actuel. + Cet événement ne peut pas être retiré dans son état actuel. + Les paramètres du guide ne sont pas configurés. diff --git a/src/Sections/Humans.Events/EventsResource.it.resx b/src/Sections/Humans.Events/EventsResource.it.resx index 28f1e9a0f8..c84533fb37 100644 --- a/src/Sections/Humans.Events/EventsResource.it.resx +++ b/src/Sections/Humans.Events/EventsResource.it.resx @@ -120,4 +120,7 @@ 1 = priorità più alta per la selezione della guida stampata. Eventi Evento ricorrente + Questo evento non può essere modificato nel suo stato attuale. + Questo evento non può essere ritirato nel suo stato attuale. + Le impostazioni della guida non sono configurate. diff --git a/src/Sections/Humans.Events/EventsResource.resx b/src/Sections/Humans.Events/EventsResource.resx index 7512cc62a8..fd1035401c 100644 --- a/src/Sections/Humans.Events/EventsResource.resx +++ b/src/Sections/Humans.Events/EventsResource.resx @@ -115,4 +115,7 @@ 1 = highest priority for print guide selection. Events Recurring event + This event cannot be edited in its current state. + This event cannot be withdrawn in its current state. + Guide settings are not configured. diff --git a/tests/Humans.Events.Tests/Controllers/EventsControllerTests.cs b/tests/Humans.Events.Tests/Controllers/EventsControllerTests.cs index ba554dacfc..7cdaf2e3fb 100644 --- a/tests/Humans.Events.Tests/Controllers/EventsControllerTests.cs +++ b/tests/Humans.Events.Tests/Controllers/EventsControllerTests.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Localization; using NodaTime; using NSubstitute; @@ -32,6 +33,7 @@ public class EventsControllerTests private readonly ICampServiceRead _camps = Substitute.For(); private readonly IAuthorizationService _authz = Substitute.For(); private readonly IClock _clock = Substitute.For(); + private readonly IStringLocalizer _localizer = Substitute.For>(); [HumansFact] public async Task Edit_NonSubmitterNonAdmin_ReturnsForbid() @@ -144,7 +146,7 @@ private EventsController BuildController(Guid currentUserId, params string[] rol _users.GetUserInfoAsync(currentUserId, Arg.Any()) .Returns(new ValueTask(MakeUserInfo(currentUserId, "Current User"))); - return new EventsController(_guide, _users, _camps, _authz, _clock, NullLogger.Instance) + return new EventsController(_guide, _users, _camps, _authz, _clock, NullLogger.Instance, _localizer) { ControllerContext = new ControllerContext { From e6d6ab8a590624289ecad292387f2e1f384dcb8d Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 09:17:31 +0200 Subject: [PATCH 23/29] Localize event submission feedback --- .../Humans.Events/Controllers/EventsController.cs | 15 +++++++++------ src/Sections/Humans.Events/EventsResource.ca.resx | 3 +++ src/Sections/Humans.Events/EventsResource.de.resx | 3 +++ src/Sections/Humans.Events/EventsResource.es.resx | 3 +++ src/Sections/Humans.Events/EventsResource.fr.resx | 3 +++ src/Sections/Humans.Events/EventsResource.it.resx | 3 +++ src/Sections/Humans.Events/EventsResource.resx | 3 +++ 7 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/Sections/Humans.Events/Controllers/EventsController.cs b/src/Sections/Humans.Events/Controllers/EventsController.cs index 24037e651b..d514a41372 100644 --- a/src/Sections/Humans.Events/Controllers/EventsController.cs +++ b/src/Sections/Humans.Events/Controllers/EventsController.cs @@ -182,7 +182,7 @@ public async Task Create(IndividualEventFormViewModel model) logger.LogInformation("User {UserId} submitted individual event '{Title}'", user.Id, model.Title); - SetSuccess($"Event \"{model.Title}\" submitted for review."); + SetSuccess(FormatEventFeedback("Events_SubmittedForReview", model.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -292,7 +292,7 @@ public async Task Update(Guid eventId, IndividualEventFormViewMod logger.LogInformation("User {UserId} updated event '{Title}' ({EventId})", user.Id, model.Title, eventId); - SetSuccess($"Event \"{model.Title}\" resubmitted for review."); + SetSuccess(FormatEventFeedback("Events_ResubmittedForReview", model.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -315,7 +315,7 @@ public async Task Withdraw(Guid eventId) await guide.WithdrawEventAsync(guideEvent); logger.LogInformation("User {UserId} withdrew event '{Title}' ({EventId})", user.Id, guideEvent.Title, eventId); - SetSuccess($"Event \"{guideEvent.Title}\" withdrawn."); + SetSuccess(FormatEventFeedback("Events_Withdrawn", guideEvent.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -588,7 +588,7 @@ public async Task BarrioCreate(string slug, CampEventFormViewMode await guide.SubmitEventAsync(guideEvent, viewUrl); logger.LogInformation("User {UserId} submitted barrio event '{Title}' for camp {CampId}", user.Id, model.Title, camp.Id); - SetSuccess($"Event \"{model.Title}\" submitted for review."); + SetSuccess(FormatEventFeedback("Events_SubmittedForReview", model.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -686,7 +686,7 @@ public async Task BarrioUpdate(string slug, Guid eventId, CampEve logger.LogInformation("User {UserId} updated barrio event '{Title}' ({EventId})", user.Id, model.Title, eventId); - SetSuccess($"Event \"{model.Title}\" resubmitted for review."); + SetSuccess(FormatEventFeedback("Events_ResubmittedForReview", model.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -709,7 +709,7 @@ public async Task BarrioWithdraw(string slug, Guid eventId) await guide.WithdrawEventAsync(guideEvent); logger.LogInformation("User {UserId} withdrew barrio event '{Title}' ({EventId})", user.Id, guideEvent.Title, eventId); - SetSuccess($"Event \"{guideEvent.Title}\" withdrawn."); + SetSuccess(FormatEventFeedback("Events_Withdrawn", guideEvent.Title)); return RedirectToAction(nameof(MySubmissions)); } @@ -792,6 +792,9 @@ public async Task BulkUploadImport(string slug, IFormFile? file) // camp is non-null at every call site; Slug is always set, so a name always resolves. private static string ResolveCampDisplayName(CampInfo camp) => ResolveCampName(camp)!; + private string FormatEventFeedback(string resourceKey, string title) => + string.Format(localizer[resourceKey].Value, title); + private async Task BuildBarrioFormAsync(string slug, CampInfo camp, EventSettingsInfo burn) { var model = new CampEventFormViewModel diff --git a/src/Sections/Humans.Events/EventsResource.ca.resx b/src/Sections/Humans.Events/EventsResource.ca.resx index accebf44a6..7ee73e22e7 100644 --- a/src/Sections/Humans.Events/EventsResource.ca.resx +++ b/src/Sections/Humans.Events/EventsResource.ca.resx @@ -123,4 +123,7 @@ Aquest esdeveniment no es pot editar en el seu estat actual. Aquest esdeveniment no es pot retirar en el seu estat actual. Els ajustos de la guia no estan configurats. + L'esdeveniment “{0}” s'ha enviat per revisar.{0} = event title + L'esdeveniment “{0}” s'ha tornat a enviar per revisar.{0} = event title + L'esdeveniment “{0}” s'ha retirat.{0} = event title diff --git a/src/Sections/Humans.Events/EventsResource.de.resx b/src/Sections/Humans.Events/EventsResource.de.resx index 1638dfa2cb..761f9803f1 100644 --- a/src/Sections/Humans.Events/EventsResource.de.resx +++ b/src/Sections/Humans.Events/EventsResource.de.resx @@ -123,4 +123,7 @@ Diese Veranstaltung kann in ihrem aktuellen Status nicht bearbeitet werden. Diese Veranstaltung kann in ihrem aktuellen Status nicht zurückgezogen werden. Die Guide-Einstellungen sind nicht konfiguriert. + Veranstaltung „{0}“ zur Prüfung eingereicht.{0} = event title + Veranstaltung „{0}“ erneut zur Prüfung eingereicht.{0} = event title + Veranstaltung „{0}“ zurückgezogen.{0} = event title diff --git a/src/Sections/Humans.Events/EventsResource.es.resx b/src/Sections/Humans.Events/EventsResource.es.resx index 1e05581c85..0ed23e00df 100644 --- a/src/Sections/Humans.Events/EventsResource.es.resx +++ b/src/Sections/Humans.Events/EventsResource.es.resx @@ -123,4 +123,7 @@ Este evento no se puede editar en su estado actual. Este evento no se puede retirar en su estado actual. Los ajustes de la guía no están configurados. + Evento “{0}” enviado para revisión.{0} = event title + Evento “{0}” reenviado para revisión.{0} = event title + Evento “{0}” retirado.{0} = event title diff --git a/src/Sections/Humans.Events/EventsResource.fr.resx b/src/Sections/Humans.Events/EventsResource.fr.resx index 459c202dbb..6fac8dadc7 100644 --- a/src/Sections/Humans.Events/EventsResource.fr.resx +++ b/src/Sections/Humans.Events/EventsResource.fr.resx @@ -123,4 +123,7 @@ Cet événement ne peut pas être modifié dans son état actuel. Cet événement ne peut pas être retiré dans son état actuel. Les paramètres du guide ne sont pas configurés. + Événement « {0} » soumis à révision.{0} = event title + Événement « {0} » soumis à nouveau à révision.{0} = event title + Événement « {0} » retiré.{0} = event title diff --git a/src/Sections/Humans.Events/EventsResource.it.resx b/src/Sections/Humans.Events/EventsResource.it.resx index c84533fb37..1f5531f398 100644 --- a/src/Sections/Humans.Events/EventsResource.it.resx +++ b/src/Sections/Humans.Events/EventsResource.it.resx @@ -123,4 +123,7 @@ Questo evento non può essere modificato nel suo stato attuale. Questo evento non può essere ritirato nel suo stato attuale. Le impostazioni della guida non sono configurate. + Evento “{0}” inviato per la revisione.{0} = event title + Evento “{0}” inviato di nuovo per la revisione.{0} = event title + Evento “{0}” ritirato.{0} = event title diff --git a/src/Sections/Humans.Events/EventsResource.resx b/src/Sections/Humans.Events/EventsResource.resx index fd1035401c..486a0868ea 100644 --- a/src/Sections/Humans.Events/EventsResource.resx +++ b/src/Sections/Humans.Events/EventsResource.resx @@ -118,4 +118,7 @@ This event cannot be edited in its current state. This event cannot be withdrawn in its current state. Guide settings are not configured. + Event “{0}” submitted for review.{0} = event title + Event “{0}” resubmitted for review.{0} = event title + Event “{0}” withdrawn.{0} = event title From 9a0ed28d787518f0b2e21784c91214911fff1417 Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 09:19:53 +0200 Subject: [PATCH 24/29] Localize event upload validation --- src/Sections/Humans.Events/Controllers/EventsController.cs | 4 ++-- src/Sections/Humans.Events/EventsResource.ca.resx | 2 ++ src/Sections/Humans.Events/EventsResource.de.resx | 2 ++ src/Sections/Humans.Events/EventsResource.es.resx | 2 ++ src/Sections/Humans.Events/EventsResource.fr.resx | 2 ++ src/Sections/Humans.Events/EventsResource.it.resx | 2 ++ src/Sections/Humans.Events/EventsResource.resx | 2 ++ 7 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Sections/Humans.Events/Controllers/EventsController.cs b/src/Sections/Humans.Events/Controllers/EventsController.cs index d514a41372..09f2f7f427 100644 --- a/src/Sections/Humans.Events/Controllers/EventsController.cs +++ b/src/Sections/Humans.Events/Controllers/EventsController.cs @@ -740,7 +740,7 @@ public async Task BulkUploadImport(string slug, IFormFile? file) if (file == null || file.Length == 0) { - SetError("Please select a CSV file to upload."); + SetError(localizer["Events_UploadSelectCsv"].Value); return RedirectToAction(nameof(MySubmissions)); } @@ -764,7 +764,7 @@ public async Task BulkUploadImport(string slug, IFormFile? file) if (rows.Count == 0) { - SetError("The CSV had no event rows. Add at least one row below the header and try again."); + SetError(localizer["Events_UploadNoRows"].Value); return RedirectToAction(nameof(MySubmissions)); } diff --git a/src/Sections/Humans.Events/EventsResource.ca.resx b/src/Sections/Humans.Events/EventsResource.ca.resx index 7ee73e22e7..2454ad8458 100644 --- a/src/Sections/Humans.Events/EventsResource.ca.resx +++ b/src/Sections/Humans.Events/EventsResource.ca.resx @@ -126,4 +126,6 @@ L'esdeveniment “{0}” s'ha enviat per revisar.{0} = event title L'esdeveniment “{0}” s'ha tornat a enviar per revisar.{0} = event title L'esdeveniment “{0}” s'ha retirat.{0} = event title + Selecciona un fitxer CSV per pujar. + El CSV no tenia cap fila d'esdeveniment. Afegeix almenys una fila sota la capçalera i torna-ho a provar. diff --git a/src/Sections/Humans.Events/EventsResource.de.resx b/src/Sections/Humans.Events/EventsResource.de.resx index 761f9803f1..46af6da605 100644 --- a/src/Sections/Humans.Events/EventsResource.de.resx +++ b/src/Sections/Humans.Events/EventsResource.de.resx @@ -126,4 +126,6 @@ Veranstaltung „{0}“ zur Prüfung eingereicht.{0} = event title Veranstaltung „{0}“ erneut zur Prüfung eingereicht.{0} = event title Veranstaltung „{0}“ zurückgezogen.{0} = event title + Bitte wähle eine CSV-Datei zum Hochladen aus. + Die CSV enthielt keine Veranstaltungszeilen. Füge mindestens eine Zeile unter der Kopfzeile hinzu und versuche es erneut. diff --git a/src/Sections/Humans.Events/EventsResource.es.resx b/src/Sections/Humans.Events/EventsResource.es.resx index 0ed23e00df..7ad4fc6f42 100644 --- a/src/Sections/Humans.Events/EventsResource.es.resx +++ b/src/Sections/Humans.Events/EventsResource.es.resx @@ -126,4 +126,6 @@ Evento “{0}” enviado para revisión.{0} = event title Evento “{0}” reenviado para revisión.{0} = event title Evento “{0}” retirado.{0} = event title + Selecciona un archivo CSV para subir. + El CSV no tenía filas de eventos. Añade al menos una fila debajo de la cabecera e inténtalo de nuevo. diff --git a/src/Sections/Humans.Events/EventsResource.fr.resx b/src/Sections/Humans.Events/EventsResource.fr.resx index 6fac8dadc7..ffb9c7a727 100644 --- a/src/Sections/Humans.Events/EventsResource.fr.resx +++ b/src/Sections/Humans.Events/EventsResource.fr.resx @@ -126,4 +126,6 @@ Événement « {0} » soumis à révision.{0} = event title Événement « {0} » soumis à nouveau à révision.{0} = event title Événement « {0} » retiré.{0} = event title + Veuillez sélectionner un fichier CSV à téléverser. + Le CSV ne contenait aucune ligne d'événement. Ajoutez au moins une ligne sous l'en-tête et réessayez. diff --git a/src/Sections/Humans.Events/EventsResource.it.resx b/src/Sections/Humans.Events/EventsResource.it.resx index 1f5531f398..5a28f9ff83 100644 --- a/src/Sections/Humans.Events/EventsResource.it.resx +++ b/src/Sections/Humans.Events/EventsResource.it.resx @@ -126,4 +126,6 @@ Evento “{0}” inviato per la revisione.{0} = event title Evento “{0}” inviato di nuovo per la revisione.{0} = event title Evento “{0}” ritirato.{0} = event title + Seleziona un file CSV da caricare. + Il CSV non conteneva righe di eventi. Aggiungi almeno una riga sotto l'intestazione e riprova. diff --git a/src/Sections/Humans.Events/EventsResource.resx b/src/Sections/Humans.Events/EventsResource.resx index 486a0868ea..ca06ad7ac8 100644 --- a/src/Sections/Humans.Events/EventsResource.resx +++ b/src/Sections/Humans.Events/EventsResource.resx @@ -121,4 +121,6 @@ Event “{0}” submitted for review.{0} = event title Event “{0}” resubmitted for review.{0} = event title Event “{0}” withdrawn.{0} = event title + Please select a CSV file to upload. + The CSV had no event rows. Add at least one row below the header and try again. From e6732875412ec425ad92f8bdc8d629c9b817ad8e Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 09:22:42 +0200 Subject: [PATCH 25/29] Localize event upload completion --- src/Sections/Humans.Events/Controllers/EventsController.cs | 6 +++++- src/Sections/Humans.Events/EventsResource.ca.resx | 1 + src/Sections/Humans.Events/EventsResource.de.resx | 1 + src/Sections/Humans.Events/EventsResource.es.resx | 1 + src/Sections/Humans.Events/EventsResource.fr.resx | 1 + src/Sections/Humans.Events/EventsResource.it.resx | 1 + src/Sections/Humans.Events/EventsResource.resx | 1 + 7 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Sections/Humans.Events/Controllers/EventsController.cs b/src/Sections/Humans.Events/Controllers/EventsController.cs index 09f2f7f427..3023a8fe56 100644 --- a/src/Sections/Humans.Events/Controllers/EventsController.cs +++ b/src/Sections/Humans.Events/Controllers/EventsController.cs @@ -784,7 +784,11 @@ public async Task BulkUploadImport(string slug, IFormFile? file) logger.LogInformation( "Bulk upload by user {UserId} for camp {CampId}: {Created} created, {Updated} updated.", user.Id, camp.Id, result.CreatedCount, result.UpdatedCount); - SetSuccess($"Bulk upload complete — {result.CreatedCount} created, {result.UpdatedCount} updated."); + SetSuccess(string.Format( + System.Globalization.CultureInfo.CurrentCulture, + localizer["Events_UploadComplete"].Value, + result.CreatedCount, + result.UpdatedCount)); return RedirectToAction(nameof(MySubmissions)); } diff --git a/src/Sections/Humans.Events/EventsResource.ca.resx b/src/Sections/Humans.Events/EventsResource.ca.resx index 2454ad8458..2780d22b61 100644 --- a/src/Sections/Humans.Events/EventsResource.ca.resx +++ b/src/Sections/Humans.Events/EventsResource.ca.resx @@ -128,4 +128,5 @@ L'esdeveniment “{0}” s'ha retirat.{0} = event title Selecciona un fitxer CSV per pujar. El CSV no tenia cap fila d'esdeveniment. Afegeix almenys una fila sota la capçalera i torna-ho a provar. + Càrrega massiva completada: {0} creats, {1} actualitzats.{0} = created count; {1} = updated count diff --git a/src/Sections/Humans.Events/EventsResource.de.resx b/src/Sections/Humans.Events/EventsResource.de.resx index 46af6da605..eeb7dd780a 100644 --- a/src/Sections/Humans.Events/EventsResource.de.resx +++ b/src/Sections/Humans.Events/EventsResource.de.resx @@ -128,4 +128,5 @@ Veranstaltung „{0}“ zurückgezogen.{0} = event title Bitte wähle eine CSV-Datei zum Hochladen aus. Die CSV enthielt keine Veranstaltungszeilen. Füge mindestens eine Zeile unter der Kopfzeile hinzu und versuche es erneut. + Massen-Upload abgeschlossen — {0} erstellt, {1} aktualisiert.{0} = created count; {1} = updated count diff --git a/src/Sections/Humans.Events/EventsResource.es.resx b/src/Sections/Humans.Events/EventsResource.es.resx index 7ad4fc6f42..6acc61f3a9 100644 --- a/src/Sections/Humans.Events/EventsResource.es.resx +++ b/src/Sections/Humans.Events/EventsResource.es.resx @@ -128,4 +128,5 @@ Evento “{0}” retirado.{0} = event title Selecciona un archivo CSV para subir. El CSV no tenía filas de eventos. Añade al menos una fila debajo de la cabecera e inténtalo de nuevo. + Carga masiva completada: {0} creados, {1} actualizados.{0} = created count; {1} = updated count diff --git a/src/Sections/Humans.Events/EventsResource.fr.resx b/src/Sections/Humans.Events/EventsResource.fr.resx index ffb9c7a727..00a49ca174 100644 --- a/src/Sections/Humans.Events/EventsResource.fr.resx +++ b/src/Sections/Humans.Events/EventsResource.fr.resx @@ -128,4 +128,5 @@ Événement « {0} » retiré.{0} = event title Veuillez sélectionner un fichier CSV à téléverser. Le CSV ne contenait aucune ligne d'événement. Ajoutez au moins une ligne sous l'en-tête et réessayez. + Importation en masse terminée : {0} créés, {1} mis à jour.{0} = created count; {1} = updated count diff --git a/src/Sections/Humans.Events/EventsResource.it.resx b/src/Sections/Humans.Events/EventsResource.it.resx index 5a28f9ff83..f7db3109a7 100644 --- a/src/Sections/Humans.Events/EventsResource.it.resx +++ b/src/Sections/Humans.Events/EventsResource.it.resx @@ -128,4 +128,5 @@ Evento “{0}” ritirato.{0} = event title Seleziona un file CSV da caricare. Il CSV non conteneva righe di eventi. Aggiungi almeno una riga sotto l'intestazione e riprova. + Caricamento di massa completato: {0} creati, {1} aggiornati.{0} = created count; {1} = updated count diff --git a/src/Sections/Humans.Events/EventsResource.resx b/src/Sections/Humans.Events/EventsResource.resx index ca06ad7ac8..8bd5db9777 100644 --- a/src/Sections/Humans.Events/EventsResource.resx +++ b/src/Sections/Humans.Events/EventsResource.resx @@ -123,4 +123,5 @@ Event “{0}” withdrawn.{0} = event title Please select a CSV file to upload. The CSV had no event rows. Add at least one row below the header and try again. + Bulk upload complete — {0} created, {1} updated.{0} = created count; {1} = updated count From 06341021317409a62dfaa25361bb2a9a26bd0c4f Mon Sep 17 00:00:00 2001 From: Peter Drier Date: Tue, 22 Sep 2026 09:26:13 +0200 Subject: [PATCH 26/29] Localize guest data export failure --- .../Humans.Gdpr/Controllers/GuestDataController.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Sections/Humans.Gdpr/Controllers/GuestDataController.cs b/src/Sections/Humans.Gdpr/Controllers/GuestDataController.cs index a959c65df3..f5ce52570e 100644 --- a/src/Sections/Humans.Gdpr/Controllers/GuestDataController.cs +++ b/src/Sections/Humans.Gdpr/Controllers/GuestDataController.cs @@ -1,10 +1,12 @@ using Humans.Base.Controllers; using Humans.Base.Extensions; +using Humans.Base; using Humans.Gdpr.Contracts; using Humans.Users.Contracts; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Localization; using NodaTime; namespace Humans.Gdpr.Controllers; @@ -23,7 +25,8 @@ internal sealed class GuestDataController( IUserServiceRead userService, IGdprService gdprExportService, IClock clock, - ILogger logger) : HumansControllerBase(userService) + ILogger logger, + IStringLocalizer localizer) : HumansControllerBase(userService) { private static readonly System.Text.Json.JsonSerializerOptions ExportJsonOptions = new() { @@ -54,7 +57,7 @@ public async Task DownloadData(CancellationToken ct) catch (Exception ex) { logger.LogError(ex, "Failed to export data for user {UserId}", user.Id); - SetError("Failed to export data. Please try again."); + SetError(localizer["Error_TryAgainLater"].Value); return RedirectToAction("Index", "Guest"); } } From c5cd1bd0d07e6853b17126628430d218a72bad87 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 21:18:25 +0000 Subject: [PATCH 27/29] Debt review: revert 2, repair 8, fix 5 findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverted: a4977e5a Move Google resource sort to service — bulk read lost ProvisionedAt order that group sync's First() relies on Reverted: c21664c3 CONT-2 localize container errors — CityPlanningController still shows ex.Message, now a raw key Repaired: 60d7c440 rideshare notifications — render inside CultureScope so dates follow recipient; drop orphaned MineLabel Repaired: 25e57db1, 566313ed EventSubmission_* -> Events_Submission_* Repaired: 9ecf0a4a AdminEditTeam_* (new keys) -> Teams_EditTeam_* Repaired: 59c6bdae, 0961c356, 6acbf496 new Profile_*/Guest_* -> Users_Profile_*/Users_Guest_* Repaired: 839c0747 ShiftProfile_* -> Shifts_ShiftProfile_* Repaired: 03623fbf new TicketTransfer_* -> Tickets_TicketTransfer_* Fixed: RideshareService.cs:673 recipient culture never applied (Claude) Fixed: RideshareService.cs:276 notification dates in actor culture (Codex) Fixed: RideshareService.cs:677 orphaned MineLabel (Codex) Fixed: TeamResourceService.cs:37 bulk read unordered (Codex, via revert) Fixed: EventsResource.resx:91 new keys lack section prefix (Codex) Review-round: 1 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012jUyNBoz1pjgWZmmTPryHr --- .../ContainersResource.ca.resx | 6 --- .../ContainersResource.de.resx | 6 --- .../ContainersResource.es.resx | 6 --- .../ContainersResource.fr.resx | 6 --- .../ContainersResource.it.resx | 6 --- .../Humans.Containers/ContainersResource.resx | 2 - .../Controllers/ContainerController.cs | 3 +- src/Sections/Humans.Containers/Docs/debt.yml | 4 ++ .../Humans.Containers/Services/Service.cs | 7 ++- .../Humans.Events/EventsResource.ca.resx | 50 +++++++++---------- .../Humans.Events/EventsResource.de.resx | 50 +++++++++---------- .../Humans.Events/EventsResource.es.resx | 50 +++++++++---------- .../Humans.Events/EventsResource.fr.resx | 50 +++++++++---------- .../Humans.Events/EventsResource.it.resx | 50 +++++++++---------- .../Humans.Events/EventsResource.resx | 50 +++++++++---------- .../Views/Events/BarrioEventForm.cshtml | 36 ++++++------- .../Views/Events/IndividualEventForm.cshtml | 36 ++++++------- .../Data/GoogleResourceRepository.cs | 2 + .../Services/TeamResourceService.cs | 2 +- .../Services/RideshareService.cs | 14 ++++-- .../Controllers/ShiftProfileController.cs | 4 +- .../Humans.Shifts/ShiftsResource.ca.resx | 4 +- .../Humans.Shifts/ShiftsResource.de.resx | 4 +- .../Humans.Shifts/ShiftsResource.es.resx | 4 +- .../Humans.Shifts/ShiftsResource.fr.resx | 4 +- .../Humans.Shifts/ShiftsResource.it.resx | 4 +- .../Humans.Shifts/ShiftsResource.resx | 4 +- .../Humans.Teams/TeamsResource.ca.resx | 30 +++++------ .../Humans.Teams/TeamsResource.de.resx | 30 +++++------ .../Humans.Teams/TeamsResource.es.resx | 30 +++++------ .../Humans.Teams/TeamsResource.fr.resx | 30 +++++------ .../Humans.Teams/TeamsResource.it.resx | 30 +++++------ src/Sections/Humans.Teams/TeamsResource.resx | 30 +++++------ .../Humans.Teams/Views/Team/EditTeam.cshtml | 28 +++++------ .../Controllers/TicketTransferController.cs | 6 +-- .../Humans.Tickets/TicketsResource.ca.resx | 6 +-- .../Humans.Tickets/TicketsResource.de.resx | 6 +-- .../Humans.Tickets/TicketsResource.es.resx | 6 +-- .../Humans.Tickets/TicketsResource.fr.resx | 6 +-- .../Humans.Tickets/TicketsResource.it.resx | 6 +-- .../Humans.Tickets/TicketsResource.resx | 6 +-- .../Controllers/GuestAccountController.cs | 6 +-- .../Controllers/ProfileController.cs | 2 +- .../Controllers/ProfileEmailsController.cs | 4 +- .../Controllers/ProfileViewController.cs | 4 +- .../Models/GuestDeletionRequestFlash.cs | 4 +- .../Humans.Users/UsersResource.ca.resx | 16 +++--- .../Humans.Users/UsersResource.de.resx | 16 +++--- .../Humans.Users/UsersResource.es.resx | 16 +++--- .../Humans.Users/UsersResource.fr.resx | 16 +++--- .../Humans.Users/UsersResource.it.resx | 16 +++--- src/Sections/Humans.Users/UsersResource.resx | 16 +++--- .../Services/ServiceImageTests.cs | 8 +-- .../GoogleResourceRepositoryTests.cs | 4 +- .../GuestAccountControllerTests.cs | 2 +- .../DisplaySortInControllers.baseline.txt | 2 + 56 files changed, 413 insertions(+), 433 deletions(-) diff --git a/src/Sections/Humans.Containers/ContainersResource.ca.resx b/src/Sections/Humans.Containers/ContainersResource.ca.resx index baa3402ae5..69fb1cd979 100644 --- a/src/Sections/Humans.Containers/ContainersResource.ca.resx +++ b/src/Sections/Humans.Containers/ContainersResource.ca.resx @@ -123,12 +123,6 @@ Contenidor eliminat. - - El nom del contenidor no pot contenir <, > ni $. - - - Un contenidor pot tenir com a màxim 5 imatges. - Anterior diff --git a/src/Sections/Humans.Containers/ContainersResource.de.resx b/src/Sections/Humans.Containers/ContainersResource.de.resx index 8dec425165..45612a82e8 100644 --- a/src/Sections/Humans.Containers/ContainersResource.de.resx +++ b/src/Sections/Humans.Containers/ContainersResource.de.resx @@ -123,12 +123,6 @@ Container gelöscht. - - Der Containername darf <, > oder $ nicht enthalten. - - - Ein Container darf höchstens 5 Bilder haben. - Zurück diff --git a/src/Sections/Humans.Containers/ContainersResource.es.resx b/src/Sections/Humans.Containers/ContainersResource.es.resx index 8432f1f02a..be2d7940da 100644 --- a/src/Sections/Humans.Containers/ContainersResource.es.resx +++ b/src/Sections/Humans.Containers/ContainersResource.es.resx @@ -123,12 +123,6 @@ Contenedor eliminado. - - El nombre del contenedor no puede contener <, > ni $. - - - Un contenedor puede tener como máximo 5 imágenes. - Anterior diff --git a/src/Sections/Humans.Containers/ContainersResource.fr.resx b/src/Sections/Humans.Containers/ContainersResource.fr.resx index 34332f892c..709dd7de5e 100644 --- a/src/Sections/Humans.Containers/ContainersResource.fr.resx +++ b/src/Sections/Humans.Containers/ContainersResource.fr.resx @@ -123,12 +123,6 @@ Conteneur supprimé. - - Le nom du conteneur ne doit pas contenir <, > ni $. - - - Un conteneur peut avoir au maximum 5 images. - Précédent diff --git a/src/Sections/Humans.Containers/ContainersResource.it.resx b/src/Sections/Humans.Containers/ContainersResource.it.resx index 9a38e6e7e3..2704d5dba3 100644 --- a/src/Sections/Humans.Containers/ContainersResource.it.resx +++ b/src/Sections/Humans.Containers/ContainersResource.it.resx @@ -123,12 +123,6 @@ Contenitore eliminato. - - Il nome del container non può contenere <, > o $. - - - Un container può avere al massimo 5 immagini. - Precedente diff --git a/src/Sections/Humans.Containers/ContainersResource.resx b/src/Sections/Humans.Containers/ContainersResource.resx index 6b1336a69c..02e0346d08 100644 --- a/src/Sections/Humans.Containers/ContainersResource.resx +++ b/src/Sections/Humans.Containers/ContainersResource.resx @@ -63,8 +63,6 @@ Container added. Container updated. Container deleted. - Container name must not contain <, >, or $. - A container can have at most 5 images. Previous Next diff --git a/src/Sections/Humans.Containers/Controllers/ContainerController.cs b/src/Sections/Humans.Containers/Controllers/ContainerController.cs index afba2903b3..3e7b806b97 100644 --- a/src/Sections/Humans.Containers/Controllers/ContainerController.cs +++ b/src/Sections/Humans.Containers/Controllers/ContainerController.cs @@ -159,8 +159,7 @@ private async Task TryRunContainerWriteAsync( catch (InvalidOperationException ex) { logger.LogWarning("Container write failed for camp {Slug}: {Message}", slug, ex.Message); - var localized = localizer[ex.Message]; - SetError(localized.ResourceNotFound ? ex.Message : localized.Value); + SetError(ex.Message); return RedirectToAction(nameof(Index), new { slug }); } diff --git a/src/Sections/Humans.Containers/Docs/debt.yml b/src/Sections/Humans.Containers/Docs/debt.yml index 8ac002be5d..5715825373 100644 --- a/src/Sections/Humans.Containers/Docs/debt.yml +++ b/src/Sections/Humans.Containers/Docs/debt.yml @@ -8,3 +8,7 @@ inbox: what: "No controller tests for ContainerController (finding 13, /section-doctor on Containers 2026-09-08): the 403 on /Camp/{slug}/Containers for a non-lead, the Forbid on Create/Edit/Delete, the NotFound on an unknown slug or id, and the ModelState → flash → redirect path are unpinned end to end; the handler is unit-tested by hand-built context only. Needs the controller-test scaffolding other sections use (HumansControllerBase + IAuthorizationService substitute)." review: panel root: CENTRAL-56 + - added: 2026-09-14 + id: CONT-2 + what: "Service validation failures reach the member untranslated: ContainerController's catch hands ex.Message to SetError, so \"A container can have at most 5 images.\" and the name-character message render in English in every culture (finding 5, /section-doctor on Containers 2026-09-08). Peter 2026-09-14: accept English for now, do the error-key pass later — Service throws a key the controller localizes against ContainersResource, rather than a formatted sentence. The two messages live in Service.ValidateName and the image-count guard." + review: light diff --git a/src/Sections/Humans.Containers/Services/Service.cs b/src/Sections/Humans.Containers/Services/Service.cs index f5bcaac039..8f5ca4f91f 100644 --- a/src/Sections/Humans.Containers/Services/Service.cs +++ b/src/Sections/Humans.Containers/Services/Service.cs @@ -25,8 +25,6 @@ internal sealed class Service( new(StringComparer.OrdinalIgnoreCase) { ".jpg", ".jpeg", ".png", ".webp" }; private const long MaxImageBytes = 10 * 1024 * 1024; private const int MaxImagesPerContainer = 5; - internal const string InvalidNameError = "Containers_Error_InvalidName"; - internal const string TooManyImagesError = "Containers_Error_TooManyImages"; public async Task> GetByCampAsync(Guid campId, CancellationToken ct = default) { @@ -286,7 +284,7 @@ private static void ValidateName(string name) { if (name.IndexOfAny(InvalidNameChars) >= 0) { - throw new InvalidOperationException(InvalidNameError); + throw new InvalidOperationException("Container name must not contain <, > or $."); } } @@ -294,7 +292,8 @@ private static void ValidateImageCount(int total) { if (total > MaxImagesPerContainer) { - throw new InvalidOperationException(TooManyImagesError); + throw new InvalidOperationException( + $"A container can have at most {MaxImagesPerContainer} images."); } } diff --git a/src/Sections/Humans.Events/EventsResource.ca.resx b/src/Sections/Humans.Events/EventsResource.ca.resx index 2780d22b61..007ab725e1 100644 --- a/src/Sections/Humans.Events/EventsResource.ca.resx +++ b/src/Sections/Humans.Events/EventsResource.ca.resx @@ -93,31 +93,31 @@ La pujada ha fallat. Corregeix els errors de sota i torna-ho a provar. No s'ha desat cap esdeveniment. Vols retirar aquest esdeveniment? - Edita l'esdeveniment - Envia un esdeveniment - Edita - Nou - Aquest esdeveniment s'ha retornat per editar-lo. Actualitza'n els detalls i torna'l a enviar. - caràcters restants - S'admet el format Markdown - — Selecciona una categoria — - — Selecciona un lloc — - Esdeveniment de tot el dia - — Selecciona un dia — - Aquest esdeveniment es repeteix diversos dies - Dies de repetició - p. ex., prop de la foguera - p. ex., El Col·lectiu del Te - Es mostra a la guia en lloc del teu nom quan s'omple. - Torna a enviar - Envia per revisar - Cancel·la - Edita l'esdeveniment — {0} - Envia l'esdeveniment — {0} - Envia un esdeveniment nou - p. ex., Luna - Opcional: indica la persona que organitza aquest esdeveniment. - 1 = prioritat més alta per a la selecció de la guia impresa. + Edita l'esdeveniment + Envia un esdeveniment + Edita + Nou + Aquest esdeveniment s'ha retornat per editar-lo. Actualitza'n els detalls i torna'l a enviar. + caràcters restants + S'admet el format Markdown + — Selecciona una categoria — + — Selecciona un lloc — + Esdeveniment de tot el dia + — Selecciona un dia — + Aquest esdeveniment es repeteix diversos dies + Dies de repetició + p. ex., prop de la foguera + p. ex., El Col·lectiu del Te + Es mostra a la guia en lloc del teu nom quan s'omple. + Torna a enviar + Envia per revisar + Cancel·la + Edita l'esdeveniment — {0} + Envia l'esdeveniment — {0} + Envia un esdeveniment nou + p. ex., Luna + Opcional: indica la persona que organitza aquest esdeveniment. + 1 = prioritat més alta per a la selecció de la guia impresa. Esdeveniments Esdeveniment recurrent Aquest esdeveniment no es pot editar en el seu estat actual. diff --git a/src/Sections/Humans.Events/EventsResource.de.resx b/src/Sections/Humans.Events/EventsResource.de.resx index eeb7dd780a..469b7a627e 100644 --- a/src/Sections/Humans.Events/EventsResource.de.resx +++ b/src/Sections/Humans.Events/EventsResource.de.resx @@ -93,31 +93,31 @@ Upload fehlgeschlagen. Behebe die folgenden Fehler und versuche es erneut. Es wurden keine Veranstaltungen gespeichert. Diese Veranstaltung zurückziehen? - Veranstaltung bearbeiten - Veranstaltung einreichen - Bearbeiten - Neu - Diese Veranstaltung wurde zur Bearbeitung zurückgegeben. Aktualisiere die Angaben und reiche sie erneut ein. - Zeichen verbleiben - Markdown-Formatierung wird unterstützt - — Kategorie auswählen — - — Ort auswählen — - Ganztägige Veranstaltung - — Tag auswählen — - Diese Veranstaltung wiederholt sich an mehreren Tagen - Wiederholungstage - z. B. nahe der Feuerstelle - z. B. das Teekollektiv - Wird ausgefüllt im Guide statt deines Namens angezeigt. - Erneut einreichen - Zur Prüfung einreichen - Abbrechen - Veranstaltung bearbeiten — {0} - Veranstaltung einreichen — {0} - Neue Veranstaltung einreichen - z. B. Luna - Optional — benennt die Person, die diese Veranstaltung leitet. - 1 = höchste Priorität für die Auswahl im gedruckten Guide. + Veranstaltung bearbeiten + Veranstaltung einreichen + Bearbeiten + Neu + Diese Veranstaltung wurde zur Bearbeitung zurückgegeben. Aktualisiere die Angaben und reiche sie erneut ein. + Zeichen verbleiben + Markdown-Formatierung wird unterstützt + — Kategorie auswählen — + — Ort auswählen — + Ganztägige Veranstaltung + — Tag auswählen — + Diese Veranstaltung wiederholt sich an mehreren Tagen + Wiederholungstage + z. B. nahe der Feuerstelle + z. B. das Teekollektiv + Wird ausgefüllt im Guide statt deines Namens angezeigt. + Erneut einreichen + Zur Prüfung einreichen + Abbrechen + Veranstaltung bearbeiten — {0} + Veranstaltung einreichen — {0} + Neue Veranstaltung einreichen + z. B. Luna + Optional — benennt die Person, die diese Veranstaltung leitet. + 1 = höchste Priorität für die Auswahl im gedruckten Guide. Veranstaltungen Wiederkehrende Veranstaltung Diese Veranstaltung kann in ihrem aktuellen Status nicht bearbeitet werden. diff --git a/src/Sections/Humans.Events/EventsResource.es.resx b/src/Sections/Humans.Events/EventsResource.es.resx index 6acc61f3a9..18be852d24 100644 --- a/src/Sections/Humans.Events/EventsResource.es.resx +++ b/src/Sections/Humans.Events/EventsResource.es.resx @@ -93,31 +93,31 @@ La subida falló. Corrige los errores de abajo e inténtalo de nuevo. No se guardó ningún evento. ¿Retirar este evento? - Editar evento - Enviar un evento - Editar - Nuevo - Este evento se devolvió para editarlo. Actualiza los detalles y vuelve a enviarlo. - caracteres restantes - Se admite formato Markdown - — Selecciona una categoría — - — Selecciona un lugar — - Evento de todo el día - — Selecciona un día — - Este evento se repite varios días - Días de repetición - p. ej., cerca de la hoguera - p. ej., El Colectivo del Té - Se muestra en la guía en lugar de tu nombre cuando se completa. - Volver a enviar - Enviar para revisión - Cancelar - Editar evento — {0} - Enviar evento — {0} - Enviar nuevo evento - p. ej., Luna - Opcional; indica la persona que organiza este evento. - 1 = prioridad máxima para la selección de la guía impresa. + Editar evento + Enviar un evento + Editar + Nuevo + Este evento se devolvió para editarlo. Actualiza los detalles y vuelve a enviarlo. + caracteres restantes + Se admite formato Markdown + — Selecciona una categoría — + — Selecciona un lugar — + Evento de todo el día + — Selecciona un día — + Este evento se repite varios días + Días de repetición + p. ej., cerca de la hoguera + p. ej., El Colectivo del Té + Se muestra en la guía en lugar de tu nombre cuando se completa. + Volver a enviar + Enviar para revisión + Cancelar + Editar evento — {0} + Enviar evento — {0} + Enviar nuevo evento + p. ej., Luna + Opcional; indica la persona que organiza este evento. + 1 = prioridad máxima para la selección de la guía impresa. Eventos Evento recurrente Este evento no se puede editar en su estado actual. diff --git a/src/Sections/Humans.Events/EventsResource.fr.resx b/src/Sections/Humans.Events/EventsResource.fr.resx index 00a49ca174..0ba31d91f4 100644 --- a/src/Sections/Humans.Events/EventsResource.fr.resx +++ b/src/Sections/Humans.Events/EventsResource.fr.resx @@ -93,31 +93,31 @@ Échec du téléversement. Corrigez les erreurs ci-dessous et réessayez. Aucun événement n'a été enregistré. Retirer cet événement ? - Modifier l'événement - Proposer un événement - Modifier - Nouveau - Cet événement a été renvoyé pour modification. Mettez les détails à jour et soumettez-le à nouveau. - caractères restants - Le formatage Markdown est pris en charge - — Sélectionner une catégorie — - — Sélectionner un lieu — - Événement sur toute la journée - — Sélectionner un jour — - Cet événement se répète sur plusieurs jours - Jours de répétition - p. ex. près du feu de camp - p. ex. le Collectif du Thé - S'affiche dans le guide à la place de votre nom lorsqu'il est renseigné. - Soumettre à nouveau - Soumettre pour examen - Annuler - Modifier l'événement — {0} - Proposer l'événement — {0} - Proposer un nouvel événement - p. ex. Luna - Facultatif — indique la personne qui anime cet événement. - 1 = priorité la plus élevée pour la sélection du guide imprimé. + Modifier l'événement + Proposer un événement + Modifier + Nouveau + Cet événement a été renvoyé pour modification. Mettez les détails à jour et soumettez-le à nouveau. + caractères restants + Le formatage Markdown est pris en charge + — Sélectionner une catégorie — + — Sélectionner un lieu — + Événement sur toute la journée + — Sélectionner un jour — + Cet événement se répète sur plusieurs jours + Jours de répétition + p. ex. près du feu de camp + p. ex. le Collectif du Thé + S'affiche dans le guide à la place de votre nom lorsqu'il est renseigné. + Soumettre à nouveau + Soumettre pour examen + Annuler + Modifier l'événement — {0} + Proposer l'événement — {0} + Proposer un nouvel événement + p. ex. Luna + Facultatif — indique la personne qui anime cet événement. + 1 = priorité la plus élevée pour la sélection du guide imprimé. Événements Événement récurrent Cet événement ne peut pas être modifié dans son état actuel. diff --git a/src/Sections/Humans.Events/EventsResource.it.resx b/src/Sections/Humans.Events/EventsResource.it.resx index f7db3109a7..f6b51a9529 100644 --- a/src/Sections/Humans.Events/EventsResource.it.resx +++ b/src/Sections/Humans.Events/EventsResource.it.resx @@ -93,31 +93,31 @@ Caricamento fallito. Correggi gli errori qui sotto e riprova. Nessun evento è stato salvato. Ritirare questo evento? - Modifica evento - Invia un evento - Modifica - Nuovo - Questo evento è stato restituito per le modifiche. Aggiorna i dettagli e invialo di nuovo. - caratteri rimanenti - La formattazione Markdown è supportata - — Seleziona una categoria — - — Seleziona un luogo — - Evento per tutto il giorno - — Seleziona un giorno — - Questo evento si ripete in più giorni - Giorni di ripetizione - ad es. vicino al falò - ad es. il Collettivo del Tè - Viene mostrato nella guida al posto del tuo nome quando compilato. - Invia di nuovo - Invia per revisione - Annulla - Modifica evento — {0} - Invia evento — {0} - Invia nuovo evento - ad es. Luna - Facoltativo: indica la persona che gestisce questo evento. - 1 = priorità più alta per la selezione della guida stampata. + Modifica evento + Invia un evento + Modifica + Nuovo + Questo evento è stato restituito per le modifiche. Aggiorna i dettagli e invialo di nuovo. + caratteri rimanenti + La formattazione Markdown è supportata + — Seleziona una categoria — + — Seleziona un luogo — + Evento per tutto il giorno + — Seleziona un giorno — + Questo evento si ripete in più giorni + Giorni di ripetizione + ad es. vicino al falò + ad es. il Collettivo del Tè + Viene mostrato nella guida al posto del tuo nome quando compilato. + Invia di nuovo + Invia per revisione + Annulla + Modifica evento — {0} + Invia evento — {0} + Invia nuovo evento + ad es. Luna + Facoltativo: indica la persona che gestisce questo evento. + 1 = priorità più alta per la selezione della guida stampata. Eventi Evento ricorrente Questo evento non può essere modificato nel suo stato attuale. diff --git a/src/Sections/Humans.Events/EventsResource.resx b/src/Sections/Humans.Events/EventsResource.resx index 8bd5db9777..8aa3f1fc2c 100644 --- a/src/Sections/Humans.Events/EventsResource.resx +++ b/src/Sections/Humans.Events/EventsResource.resx @@ -88,31 +88,31 @@ Upload failed. Fix the errors below and try again. No events were saved. Withdraw this event? - Edit Event - Submit an Event - Edit - New - This event was returned for edits. Update the details and resubmit. - characters remaining - Markdown formatting is supported - — Select category — - — Select venue — - All day event - — Select day — - This event repeats on multiple days - Recurrence days - e.g. near the fire pit - e.g. The Tea Collective - Shown in the guide instead of your name when filled in. - Resubmit - Submit for Review - Cancel - Edit Event — {0} - Submit Event — {0} - Submit New Event - e.g. Luna - Optional — names the person running this event. - 1 = highest priority for print guide selection. + Edit Event + Submit an Event + Edit + New + This event was returned for edits. Update the details and resubmit. + characters remaining + Markdown formatting is supported + — Select category — + — Select venue — + All day event + — Select day — + This event repeats on multiple days + Recurrence days + e.g. near the fire pit + e.g. The Tea Collective + Shown in the guide instead of your name when filled in. + Resubmit + Submit for Review + Cancel + Edit Event — {0} + Submit Event — {0} + Submit New Event + e.g. Luna + Optional — names the person running this event. + 1 = highest priority for print guide selection. Events Recurring event This event cannot be edited in its current state. diff --git a/src/Sections/Humans.Events/Views/Events/BarrioEventForm.cshtml b/src/Sections/Humans.Events/Views/Events/BarrioEventForm.cshtml index 97544237d9..72a7dabe80 100644 --- a/src/Sections/Humans.Events/Views/Events/BarrioEventForm.cshtml +++ b/src/Sections/Humans.Events/Views/Events/BarrioEventForm.cshtml @@ -2,8 +2,8 @@ @{ var isEdit = Model.Id.HasValue; ViewData["Title"] = isEdit - ? Localizer["EventSubmission_BarrioEditTitle", Model.CampName].Value - : Localizer["EventSubmission_BarrioSubmitTitle", Model.CampName].Value; + ? Localizer["Events_Submission_BarrioEditTitle", Model.CampName].Value + : Localizer["Events_Submission_BarrioSubmitTitle", Model.CampName].Value; } -

@(isEdit ? Localizer["EventSubmission_EditTitle"] : Localizer["EventSubmission_SubmitNew"])

+

@(isEdit ? Localizer["Events_Submission_EditTitle"] : Localizer["Events_Submission_SubmitNew"])

@if (Model.IsResubmit) {
- @Localizer["EventSubmission_ReturnedForEdits"] + @Localizer["Events_Submission_ReturnedForEdits"]
} @@ -43,17 +43,17 @@
-
@(80 - (Model.Title?.Length ?? 0)) @Localizer["EventSubmission_CharactersRemaining"]
+
@(80 - (Model.Title?.Length ?? 0)) @Localizer["Events_Submission_CharactersRemaining"]
-
@(450 - (Model.Description?.Length ?? 0)) @Localizer["EventSubmission_CharactersRemaining"]
+
@(450 - (Model.Description?.Length ?? 0)) @Localizer["Events_Submission_CharactersRemaining"]
@@ -62,7 +62,7 @@
- + @foreach (var day in Model.EventDays) { @@ -107,11 +107,11 @@
- +
diff --git a/src/Sections/Humans.Events/Views/Events/IndividualEventForm.cshtml b/src/Sections/Humans.Events/Views/Events/IndividualEventForm.cshtml index ada82a5ee5..6c94eccfda 100644 --- a/src/Sections/Humans.Events/Views/Events/IndividualEventForm.cshtml +++ b/src/Sections/Humans.Events/Views/Events/IndividualEventForm.cshtml @@ -1,22 +1,22 @@ @model Humans.Events.Models.IndividualEventFormViewModel @{ var isEdit = Model.Id.HasValue; - ViewData["Title"] = isEdit ? Localizer["EventSubmission_EditTitle"].Value : Localizer["EventSubmission_SubmitTitle"].Value; + ViewData["Title"] = isEdit ? Localizer["Events_Submission_EditTitle"].Value : Localizer["Events_Submission_SubmitTitle"].Value; } -

@(isEdit ? Localizer["EventSubmission_EditTitle"] : Localizer["EventSubmission_SubmitTitle"])

+

@(isEdit ? Localizer["Events_Submission_EditTitle"] : Localizer["Events_Submission_SubmitTitle"])

@if (Model.IsResubmit) {
- @Localizer["EventSubmission_ReturnedForEdits"] + @Localizer["Events_Submission_ReturnedForEdits"]
} @@ -38,17 +38,17 @@
-
@(80 - (Model.Title?.Length ?? 0)) @Localizer["EventSubmission_CharactersRemaining"]
+
@(80 - (Model.Title?.Length ?? 0)) @Localizer["Events_Submission_CharactersRemaining"]
-
@(450 - (Model.Description?.Length ?? 0)) @Localizer["EventSubmission_CharactersRemaining"]
+
@(450 - (Model.Description?.Length ?? 0)) @Localizer["Events_Submission_CharactersRemaining"]
@@ -57,7 +57,7 @@
- + @foreach (var venue in Model.Venues) { @@ -80,14 +80,14 @@
- +
- +
diff --git a/src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs b/src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs index 3b4ea45f4a..8e8ec86167 100644 --- a/src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs +++ b/src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs @@ -27,6 +27,7 @@ public async Task> GetActiveByTeamIdAsync(Guid tea return await ctx.GoogleResources .AsNoTracking() .Where(r => r.TeamId == teamId && r.IsActive) + .OrderBy(r => r.ProvisionedAt) .ToListAsync(ct); } @@ -43,6 +44,7 @@ public async Task>> GetA var rows = await ctx.GoogleResources .AsNoTracking() .Where(r => teamIds.Contains(r.TeamId) && r.IsActive) + .OrderBy(r => r.ProvisionedAt) .ToListAsync(ct); var result = new Dictionary>(teamIds.Count); diff --git a/src/Sections/Humans.GoogleIntegration/Services/TeamResourceService.cs b/src/Sections/Humans.GoogleIntegration/Services/TeamResourceService.cs index cb86933138..a76d7558f4 100644 --- a/src/Sections/Humans.GoogleIntegration/Services/TeamResourceService.cs +++ b/src/Sections/Humans.GoogleIntegration/Services/TeamResourceService.cs @@ -34,7 +34,7 @@ private IRoleAssignmentService RoleAssignmentService public async Task> GetTeamResourcesAsync(Guid teamId, CancellationToken ct = default) { var resources = await repository.GetActiveByTeamIdAsync(teamId, ct); - return resources.OrderBy(r => r.ProvisionedAt).Select(ToSnapshot).ToList(); + return resources.Select(ToSnapshot).ToList(); } public async Task>> GetResourcesByTeamIdsAsync( diff --git a/src/Sections/Humans.Rideshare/Services/RideshareService.cs b/src/Sections/Humans.Rideshare/Services/RideshareService.cs index 5e0028b690..7dcd361e70 100644 --- a/src/Sections/Humans.Rideshare/Services/RideshareService.cs +++ b/src/Sections/Humans.Rideshare/Services/RideshareService.cs @@ -34,7 +34,6 @@ internal sealed class RideshareService( internal const string RideshareInterests = "RideshareInterests"; private const string MineUrl = "/Rideshare/Mine"; - private const string MineLabel = "Open Rideshare"; private const string FallbackName = "A human"; private static readonly ResourceManager NoticeResources = new(typeof(RideshareResource)); @@ -669,12 +668,19 @@ private async Task NotifyAsync( try { var language = (await users.GetUserInfoAsync(recipientUserId, ct))?.PreferredLanguage ?? "en"; - var culture = CultureInfo.GetCultureInfo(language); - var (title, body) = content(culture); + string title, body, actionLabel; + // CultureScope so ambient-culture formatting (ToWeekdayDayMonth) follows the recipient too. + using (new CultureScope(language, logger)) + { + var culture = CultureInfo.CurrentUICulture; + (title, body) = content(culture); + actionLabel = Notice(culture, "Rideshare_NoticeOpen"); + } + await notifications.SendAsync( source, notificationClass, NotificationPriority.Normal, title, [recipientUserId], body: body, actionUrl: MineUrl, - actionLabel: Notice(culture, "Rideshare_NoticeOpen"), cancellationToken: ct); + actionLabel: actionLabel, cancellationToken: ct); } catch (Exception ex) { diff --git a/src/Sections/Humans.Shifts/Controllers/ShiftProfileController.cs b/src/Sections/Humans.Shifts/Controllers/ShiftProfileController.cs index 2daab40e16..f284f72f6d 100644 --- a/src/Sections/Humans.Shifts/Controllers/ShiftProfileController.cs +++ b/src/Sections/Humans.Shifts/Controllers/ShiftProfileController.cs @@ -42,7 +42,7 @@ public async Task ShiftInfo() catch (Exception ex) { logger.LogError(ex, "Failed to load shift info for user"); - SetError(shiftsLocalizer["ShiftProfile_LoadFailed"].Value); + SetError(shiftsLocalizer["Shifts_ShiftProfile_LoadFailed"].Value); return RedirectToAction("Me", "Profile"); } } @@ -74,7 +74,7 @@ public async Task ShiftInfo(ShiftInfoViewModel model) catch (Exception ex) { logger.LogError(ex, "Failed to save shift info for user"); - SetError(shiftsLocalizer["ShiftProfile_SaveFailed"].Value); + SetError(shiftsLocalizer["Shifts_ShiftProfile_SaveFailed"].Value); return View(model); } } diff --git a/src/Sections/Humans.Shifts/ShiftsResource.ca.resx b/src/Sections/Humans.Shifts/ShiftsResource.ca.resx index 159f3ef5b2..3f7514877f 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.ca.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.ca.resx @@ -1207,6 +1207,6 @@ La llista de destinataris ja coincideix amb el públic que has seleccionat. Revisa-la i prem Envia. Per a la teva informació, les teves franges en aquest torn són: Per a la teva informació, les teves franges amb aquest equip són: - No s'ha pogut carregar la informació dels torns. - No s'ha pogut desar la informació dels torns. + No s'ha pogut carregar la informació dels torns. + No s'ha pogut desar la informació dels torns. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.de.resx b/src/Sections/Humans.Shifts/ShiftsResource.de.resx index 050ed6fcc4..6504510768 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.de.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.de.resx @@ -1207,6 +1207,6 @@ Die Empfängerliste entspricht jetzt der ausgewählten Zielgruppe. Prüfe sie und klicke dann auf Senden. Zur Info, deine Schichten in diesem Dienstplan sind: Zur Info, deine Schichten bei diesem Team: - Schichtinformationen konnten nicht geladen werden. - Schichtinformationen konnten nicht gespeichert werden. + Schichtinformationen konnten nicht geladen werden. + Schichtinformationen konnten nicht gespeichert werden. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.es.resx b/src/Sections/Humans.Shifts/ShiftsResource.es.resx index 1c37df8797..8f9c0ea8b3 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.es.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.es.resx @@ -1205,6 +1205,6 @@ La lista de destinatarios ya coincide con el público que has seleccionado. Revísala y pulsa Enviar. Para tu información, tus franjas en este turno son: Para tu información, tus franjas con este equipo son: - No se pudo cargar la información de turnos. - No se pudo guardar la información de turnos. + No se pudo cargar la información de turnos. + No se pudo guardar la información de turnos. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.fr.resx b/src/Sections/Humans.Shifts/ShiftsResource.fr.resx index af6700bc69..ae4e7c809c 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.fr.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.fr.resx @@ -1207,6 +1207,6 @@ La liste des destinataires correspond maintenant au public sélectionné. Vérifie-la, puis clique sur Envoyer. Pour information, vos créneaux sur ce roulement sont : Pour information, vos créneaux avec cette équipe : - Impossible de charger les informations de créneaux. - Impossible d'enregistrer les informations de créneaux. + Impossible de charger les informations de créneaux. + Impossible d'enregistrer les informations de créneaux. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.it.resx b/src/Sections/Humans.Shifts/ShiftsResource.it.resx index 8732b946d5..208f5e7f44 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.it.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.it.resx @@ -1207,6 +1207,6 @@ L'elenco dei destinatari corrisponde ora al pubblico selezionato. Controllalo, poi premi Invia. Per tua informazione, le tue fasce in questo turno sono: Per tua informazione, le tue fasce con questo team sono: - Impossibile caricare le informazioni sui turni. - Impossibile salvare le informazioni sui turni. + Impossibile caricare le informazioni sui turni. + Impossibile salvare le informazioni sui turni. diff --git a/src/Sections/Humans.Shifts/ShiftsResource.resx b/src/Sections/Humans.Shifts/ShiftsResource.resx index 74238a389e..7b773cc524 100644 --- a/src/Sections/Humans.Shifts/ShiftsResource.resx +++ b/src/Sections/Humans.Shifts/ShiftsResource.resx @@ -412,6 +412,6 @@ The recipient list now matches the audience you selected. Check it, then press Send. FYI, your shifts on this rota are: FYI, your shifts with this team are: - Failed to load shift information. - Failed to save shift information. + Failed to load shift information. + Failed to save shift information. diff --git a/src/Sections/Humans.Teams/TeamsResource.ca.resx b/src/Sections/Humans.Teams/TeamsResource.ca.resx index 811949a3a5..bbba2d6d04 100644 --- a/src/Sections/Humans.Teams/TeamsResource.ca.resx +++ b/src/Sections/Humans.Teams/TeamsResource.ca.resx @@ -534,19 +534,19 @@ <p>El teu equip té els recursos següents:</p><ul>{0}</ul> {0} = resource list items HTML - Aquest és un equip gestionat pel sistema. Pots editar la descripció i el prefix del grup de Google. - Slug generat automàticament: - s'actualitza automàticament quan canvia el nom. - Slug personalitzat - p. ex., comunicacio - Opcional. Quan s'estableix, aquest slug s'utilitza per a l'URL de l'equip en lloc del generat automàticament. - Tots dos slugs resoldran a aquest equip. Només lletres minúscules, números i guionets. - Inclou en la planificació pressupostària - En activar-ho, aquest equip obté una categoria pressupostària sota el grup de Departaments en crear un nou any pressupostari. - Mostra a la pàgina d'equips - En activar-ho, aquest subequip també apareix al directori principal d'equips. - Equip sensible - En activar-ho, afegir o aprovar persones mostra un diàleg de confirmació amb el registre d'auditoria que es crearà. Aquesta opció no és visible per a usuaris que no són administradors. - Activa l'entrada anticipada - En activar-ho, els coordinadors d'aquest equip i els administradors d'entrada anticipada poden concedir entrada anticipada als projectes d'aquest equip. + Aquest és un equip gestionat pel sistema. Pots editar la descripció i el prefix del grup de Google. + Slug generat automàticament: + s'actualitza automàticament quan canvia el nom. + Slug personalitzat + p. ex., comunicacio + Opcional. Quan s'estableix, aquest slug s'utilitza per a l'URL de l'equip en lloc del generat automàticament. + Tots dos slugs resoldran a aquest equip. Només lletres minúscules, números i guionets. + Inclou en la planificació pressupostària + En activar-ho, aquest equip obté una categoria pressupostària sota el grup de Departaments en crear un nou any pressupostari. + Mostra a la pàgina d'equips + En activar-ho, aquest subequip també apareix al directori principal d'equips. + Equip sensible + En activar-ho, afegir o aprovar persones mostra un diàleg de confirmació amb el registre d'auditoria que es crearà. Aquesta opció no és visible per a usuaris que no són administradors. + Activa l'entrada anticipada + En activar-ho, els coordinadors d'aquest equip i els administradors d'entrada anticipada poden concedir entrada anticipada als projectes d'aquest equip. diff --git a/src/Sections/Humans.Teams/TeamsResource.de.resx b/src/Sections/Humans.Teams/TeamsResource.de.resx index c8059c3b18..4eeee8e7b0 100644 --- a/src/Sections/Humans.Teams/TeamsResource.de.resx +++ b/src/Sections/Humans.Teams/TeamsResource.de.resx @@ -534,19 +534,19 @@ <p>Dein Team hat folgende Ressourcen:</p><ul>{0}</ul> {0} = resource list items HTML - Dies ist ein systemverwaltetes Team. Du kannst die Beschreibung und das Präfix der Google-Gruppe bearbeiten. - Automatisch erzeugter Slug: - wird bei einer Namensänderung automatisch aktualisiert. - Benutzerdefinierter Slug - z. B. kommunikation - Optional. Wenn gesetzt, wird dieser Slug statt des automatisch erzeugten für die Team-URL verwendet. - Beide Slugs führen zu diesem Team. Nur Kleinbuchstaben, Zahlen und Bindestriche. - In Budgetplanung einbeziehen - Wenn aktiviert, erhält dieses Team beim Erstellen eines neuen Haushaltsjahres eine Budgetkategorie unter der Gruppe Abteilungen. - Auf der Teamseite anzeigen - Wenn aktiviert, erscheint dieses Unterteam auch im Hauptverzeichnis der Teams. - Sensibles Team - Wenn aktiviert, zeigt das Hinzufügen oder Genehmigen von Personen einen Bestätigungsdialog mit dem zu erstellenden Prüfprotokoll. Diese Einstellung ist für Nicht-Administratoren nicht sichtbar. - Frühen Einlass aktivieren - Wenn aktiviert, können die Koordinatoren dieses Teams und die Administratoren für frühen Einlass den Projekten dieses Teams frühen Einlass gewähren. + Dies ist ein systemverwaltetes Team. Du kannst die Beschreibung und das Präfix der Google-Gruppe bearbeiten. + Automatisch erzeugter Slug: + wird bei einer Namensänderung automatisch aktualisiert. + Benutzerdefinierter Slug + z. B. kommunikation + Optional. Wenn gesetzt, wird dieser Slug statt des automatisch erzeugten für die Team-URL verwendet. + Beide Slugs führen zu diesem Team. Nur Kleinbuchstaben, Zahlen und Bindestriche. + In Budgetplanung einbeziehen + Wenn aktiviert, erhält dieses Team beim Erstellen eines neuen Haushaltsjahres eine Budgetkategorie unter der Gruppe Abteilungen. + Auf der Teamseite anzeigen + Wenn aktiviert, erscheint dieses Unterteam auch im Hauptverzeichnis der Teams. + Sensibles Team + Wenn aktiviert, zeigt das Hinzufügen oder Genehmigen von Personen einen Bestätigungsdialog mit dem zu erstellenden Prüfprotokoll. Diese Einstellung ist für Nicht-Administratoren nicht sichtbar. + Frühen Einlass aktivieren + Wenn aktiviert, können die Koordinatoren dieses Teams und die Administratoren für frühen Einlass den Projekten dieses Teams frühen Einlass gewähren. diff --git a/src/Sections/Humans.Teams/TeamsResource.es.resx b/src/Sections/Humans.Teams/TeamsResource.es.resx index a639cde4bb..2903bc7385 100644 --- a/src/Sections/Humans.Teams/TeamsResource.es.resx +++ b/src/Sections/Humans.Teams/TeamsResource.es.resx @@ -534,19 +534,19 @@ <p>Tu equipo tiene los siguientes recursos:</p><ul>{0}</ul> {0} = resource list items HTML - Este es un equipo gestionado por el sistema. Puedes editar la descripción y el prefijo del grupo de Google. - Slug generado automáticamente: - se actualiza automáticamente cuando cambia el nombre. - Slug personalizado - p. ej., comunicacion - Opcional. Cuando se establece, este slug se usa para la URL del equipo en lugar del generado automáticamente. - Ambos slugs resolverán a este equipo. Solo letras minúsculas, números y guiones. - Incluir en la planificación presupuestaria - Al activarlo, este equipo obtiene una categoría presupuestaria bajo el grupo de Departamentos al crear un nuevo año presupuestario. - Mostrar en la página de equipos - Al activarlo, este subequipo también aparece en el directorio principal de equipos. - Equipo sensible - Al activarlo, añadir o aprobar personas muestra un diálogo de confirmación con el registro de auditoría que se creará. Esta opción no es visible para usuarios que no sean administradores. - Activar acceso anticipado - Al activarlo, los coordinadores de este equipo y los administradores de acceso anticipado pueden conceder acceso anticipado a los proyectos de este equipo. + Este es un equipo gestionado por el sistema. Puedes editar la descripción y el prefijo del grupo de Google. + Slug generado automáticamente: + se actualiza automáticamente cuando cambia el nombre. + Slug personalizado + p. ej., comunicacion + Opcional. Cuando se establece, este slug se usa para la URL del equipo en lugar del generado automáticamente. + Ambos slugs resolverán a este equipo. Solo letras minúsculas, números y guiones. + Incluir en la planificación presupuestaria + Al activarlo, este equipo obtiene una categoría presupuestaria bajo el grupo de Departamentos al crear un nuevo año presupuestario. + Mostrar en la página de equipos + Al activarlo, este subequipo también aparece en el directorio principal de equipos. + Equipo sensible + Al activarlo, añadir o aprobar personas muestra un diálogo de confirmación con el registro de auditoría que se creará. Esta opción no es visible para usuarios que no sean administradores. + Activar acceso anticipado + Al activarlo, los coordinadores de este equipo y los administradores de acceso anticipado pueden conceder acceso anticipado a los proyectos de este equipo. diff --git a/src/Sections/Humans.Teams/TeamsResource.fr.resx b/src/Sections/Humans.Teams/TeamsResource.fr.resx index 872fae20f1..49177e0189 100644 --- a/src/Sections/Humans.Teams/TeamsResource.fr.resx +++ b/src/Sections/Humans.Teams/TeamsResource.fr.resx @@ -534,19 +534,19 @@ <p>Votre équipe dispose des ressources suivantes :</p><ul>{0}</ul> {0} = resource list items HTML - Cette équipe est gérée par le système. Vous pouvez modifier la description et le préfixe du groupe Google. - Slug généré automatiquement : - se met à jour automatiquement lorsque le nom change. - Slug personnalisé - p. ex. communication - Facultatif. Lorsqu'il est défini, ce slug est utilisé pour l'URL de l'équipe à la place de celui généré automatiquement. - Les deux slugs renverront vers cette équipe. Lettres minuscules, chiffres et traits d'union uniquement. - Inclure dans la planification budgétaire - Lorsqu'elle est activée, cette équipe obtient une catégorie budgétaire dans le groupe Départements lors de la création d'une nouvelle année budgétaire. - Afficher sur la page des équipes - Lorsqu'elle est activée, cette sous-équipe apparaît aussi dans l'annuaire principal des équipes. - Équipe sensible - Lorsqu'elle est activée, l'ajout ou l'approbation de personnes affiche une confirmation avec l'enregistrement d'audit qui sera créé. Cette option n'est pas visible pour les non-administrateurs. - Activer l'entrée anticipée - Lorsqu'elle est activée, les coordinateurs de cette équipe et les administrateurs d'entrée anticipée peuvent accorder une entrée anticipée aux projets de cette équipe. + Cette équipe est gérée par le système. Vous pouvez modifier la description et le préfixe du groupe Google. + Slug généré automatiquement : + se met à jour automatiquement lorsque le nom change. + Slug personnalisé + p. ex. communication + Facultatif. Lorsqu'il est défini, ce slug est utilisé pour l'URL de l'équipe à la place de celui généré automatiquement. + Les deux slugs renverront vers cette équipe. Lettres minuscules, chiffres et traits d'union uniquement. + Inclure dans la planification budgétaire + Lorsqu'elle est activée, cette équipe obtient une catégorie budgétaire dans le groupe Départements lors de la création d'une nouvelle année budgétaire. + Afficher sur la page des équipes + Lorsqu'elle est activée, cette sous-équipe apparaît aussi dans l'annuaire principal des équipes. + Équipe sensible + Lorsqu'elle est activée, l'ajout ou l'approbation de personnes affiche une confirmation avec l'enregistrement d'audit qui sera créé. Cette option n'est pas visible pour les non-administrateurs. + Activer l'entrée anticipée + Lorsqu'elle est activée, les coordinateurs de cette équipe et les administrateurs d'entrée anticipée peuvent accorder une entrée anticipée aux projets de cette équipe. diff --git a/src/Sections/Humans.Teams/TeamsResource.it.resx b/src/Sections/Humans.Teams/TeamsResource.it.resx index 1d0a5f30e7..a3ad869033 100644 --- a/src/Sections/Humans.Teams/TeamsResource.it.resx +++ b/src/Sections/Humans.Teams/TeamsResource.it.resx @@ -534,19 +534,19 @@ <p>Il tuo team ha le seguenti risorse:</p><ul>{0}</ul> {0} = resource list items HTML - Questo è un team gestito dal sistema. Puoi modificare la descrizione e il prefisso del gruppo Google. - Slug generato automaticamente: - si aggiorna automaticamente quando cambia il nome. - Slug personalizzato - ad es. comunicazione - Facoltativo. Quando impostato, questo slug viene usato per l'URL del team al posto di quello generato automaticamente. - Entrambi gli slug risolveranno a questo team. Solo lettere minuscole, numeri e trattini. - Includi nella pianificazione del budget - Quando attivato, questo team ottiene una categoria di budget nel gruppo Dipartimenti alla creazione di un nuovo anno di bilancio. - Mostra nella pagina dei team - Quando attivato, questo sotto-team appare anche nella directory principale dei team. - Team sensibile - Quando attivato, l'aggiunta o l'approvazione di persone mostra una conferma con il registro di controllo che verrà creato. Questa opzione non è visibile agli utenti non amministratori. - Abilita l'ingresso anticipato - Quando attivato, i coordinatori di questo team e gli amministratori dell'ingresso anticipato possono concedere l'ingresso anticipato ai progetti di questo team. + Questo è un team gestito dal sistema. Puoi modificare la descrizione e il prefisso del gruppo Google. + Slug generato automaticamente: + si aggiorna automaticamente quando cambia il nome. + Slug personalizzato + ad es. comunicazione + Facoltativo. Quando impostato, questo slug viene usato per l'URL del team al posto di quello generato automaticamente. + Entrambi gli slug risolveranno a questo team. Solo lettere minuscole, numeri e trattini. + Includi nella pianificazione del budget + Quando attivato, questo team ottiene una categoria di budget nel gruppo Dipartimenti alla creazione di un nuovo anno di bilancio. + Mostra nella pagina dei team + Quando attivato, questo sotto-team appare anche nella directory principale dei team. + Team sensibile + Quando attivato, l'aggiunta o l'approvazione di persone mostra una conferma con il registro di controllo che verrà creato. Questa opzione non è visibile agli utenti non amministratori. + Abilita l'ingresso anticipato + Quando attivato, i coordinatori di questo team e gli amministratori dell'ingresso anticipato possono concedere l'ingresso anticipato ai progetti di questo team. diff --git a/src/Sections/Humans.Teams/TeamsResource.resx b/src/Sections/Humans.Teams/TeamsResource.resx index 2b527d9f5c..a8facd605d 100644 --- a/src/Sections/Humans.Teams/TeamsResource.resx +++ b/src/Sections/Humans.Teams/TeamsResource.resx @@ -212,19 +212,19 @@ <p><a href="{3}">View Team Page</a></p> <p>The Humans Team</p>{0} = user name, {1} = team name, {2} = resources HTML (or empty), {3} = team URL <p>Your team has the following resources:</p><ul>{0}</ul>{0} = resource list items HTML - This is a system-managed team. You can edit the description and Google Group prefix. - Auto-generated slug: - updates automatically when name changes. - Custom Slug - e.g. comms - Optional. When set, this slug is used for the team's URL instead of the auto-generated one. - Both slugs will resolve to this team. Lowercase letters, numbers, and hyphens only. - Include in budget planning - When enabled, this team gets a budget category under the Departments group when a new budget year is created. - Show on Teams page - When enabled, this sub-team also appears on the main Teams directory page. - Sensitive team - When enabled, adding or approving humans triggers a confirmation modal showing the audit record that will be created. This flag is not visible to non-admin users. - Enable Early Entry - When enabled, coordinators of this team (and Early-Entry Team Admins) can grant early entry for this team's projects. + This is a system-managed team. You can edit the description and Google Group prefix. + Auto-generated slug: + updates automatically when name changes. + Custom Slug + e.g. comms + Optional. When set, this slug is used for the team's URL instead of the auto-generated one. + Both slugs will resolve to this team. Lowercase letters, numbers, and hyphens only. + Include in budget planning + When enabled, this team gets a budget category under the Departments group when a new budget year is created. + Show on Teams page + When enabled, this sub-team also appears on the main Teams directory page. + Sensitive team + When enabled, adding or approving humans triggers a confirmation modal showing the audit record that will be created. This flag is not visible to non-admin users. + Enable Early Entry + When enabled, coordinators of this team (and Early-Entry Team Admins) can grant early entry for this team's projects. diff --git a/src/Sections/Humans.Teams/Views/Team/EditTeam.cshtml b/src/Sections/Humans.Teams/Views/Team/EditTeam.cshtml index 0c082df84e..3e8b143e76 100644 --- a/src/Sections/Humans.Teams/Views/Team/EditTeam.cshtml +++ b/src/Sections/Humans.Teams/Views/Team/EditTeam.cshtml @@ -22,7 +22,7 @@ {
- @Localizer["AdminEditTeam_SystemManagedHelp"] + @Localizer["Teams_EditTeam_SystemManagedHelp"]
} @@ -37,18 +37,18 @@ -
@Localizer["AdminEditTeam_AutoSlug"] @Model.Slug — @Localizer["AdminEditTeam_AutoSlugHelp"]
+
@Localizer["Teams_EditTeam_AutoSlug"] @Model.Slug — @Localizer["Teams_EditTeam_AutoSlugHelp"]
@if (!Model.IsSystemTeam) {
- - + +
- @Localizer["AdminEditTeam_CustomSlugHelp"] - @Localizer["AdminEditTeam_CustomSlugFormatHelp"] + @Localizer["Teams_EditTeam_CustomSlugHelp"] + @Localizer["Teams_EditTeam_CustomSlugFormatHelp"]
} @@ -64,8 +64,8 @@
- -
@Localizer["AdminEditTeam_IncludeInBudgetHelp"]
+ +
@Localizer["Teams_EditTeam_IncludeInBudgetHelp"]
} @@ -73,21 +73,21 @@ {
- -
@Localizer["AdminEditTeam_ShowInDirectoryHelp"]
+ +
@Localizer["Teams_EditTeam_ShowInDirectoryHelp"]
}
- -
@Localizer["AdminEditTeam_SensitiveHelp"]
+ +
@Localizer["Teams_EditTeam_SensitiveHelp"]
- -
@Localizer["AdminEditTeam_EnableEarlyEntryHelp"]
+ +
@Localizer["Teams_EditTeam_EnableEarlyEntryHelp"]
diff --git a/src/Sections/Humans.Tickets/Controllers/TicketTransferController.cs b/src/Sections/Humans.Tickets/Controllers/TicketTransferController.cs index 8ce8aea9cd..fdf235efc5 100644 --- a/src/Sections/Humans.Tickets/Controllers/TicketTransferController.cs +++ b/src/Sections/Humans.Tickets/Controllers/TicketTransferController.cs @@ -54,7 +54,7 @@ public async Task Confirm(Guid attendeeId, Guid receiverUserId, C HolderEarlyEntry = earlyEntry?.EarliestEntryDate, Confirm = confirm, Error = confirm is null - ? localizer["TicketTransfer_InvalidSelection"].Value + ? localizer["Tickets_TicketTransfer_InvalidSelection"].Value : null, }); } @@ -70,7 +70,7 @@ public async Task Submit(Guid attendeeId, Guid receiverUserId, st { await service.CreateRequestAsync( new TicketTransferRequestDto(attendeeId, receiverUserId, reason ?? string.Empty), user.Id, ct); - SetSuccess(localizer["TicketTransfer_RequestSubmitted"].Value); + SetSuccess(localizer["Tickets_TicketTransfer_RequestSubmitted"].Value); return RedirectToAction("Index", "Home"); } catch (InvalidOperationException ex) @@ -103,7 +103,7 @@ public async Task Cancel(Guid id, CancellationToken ct) try { await service.CancelAsync(id, user.Id, ct); - SetSuccess(localizer["TicketTransfer_Cancelled"].Value); + SetSuccess(localizer["Tickets_TicketTransfer_Cancelled"].Value); } catch (InvalidOperationException ex) { diff --git a/src/Sections/Humans.Tickets/TicketsResource.ca.resx b/src/Sections/Humans.Tickets/TicketsResource.ca.resx index 818920a7b7..7643d04728 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.ca.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.ca.resx @@ -65,7 +65,7 @@ 3. Confirma Transfereix una entrada Les teves sol·licituds de transferència - No s'ha pogut preparar la transferència: tria una de les teves entrades i una persona destinatària vàlida que no siguis tu. - Transferència sol·licitada. El nostre equip d'entrades la processarà i t'ho farà saber aviat. - Transferència cancel·lada. + No s'ha pogut preparar la transferència: tria una de les teves entrades i una persona destinatària vàlida que no siguis tu. + Transferència sol·licitada. El nostre equip d'entrades la processarà i t'ho farà saber aviat. + Transferència cancel·lada. diff --git a/src/Sections/Humans.Tickets/TicketsResource.de.resx b/src/Sections/Humans.Tickets/TicketsResource.de.resx index b1cb79872a..348977e0e3 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.de.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.de.resx @@ -65,7 +65,7 @@ 3. Bestätigen Ticket übertragen Deine Übertragungsanfragen - Die Übertragung konnte nicht vorbereitet werden — wähle eines deiner Tickets und eine gültige andere empfangende Person. - Übertragung angefragt. Unser Ticketing-Team bearbeitet sie und gibt dir in Kürze Bescheid. - Übertragung storniert. + Die Übertragung konnte nicht vorbereitet werden — wähle eines deiner Tickets und eine gültige andere empfangende Person. + Übertragung angefragt. Unser Ticketing-Team bearbeitet sie und gibt dir in Kürze Bescheid. + Übertragung storniert. diff --git a/src/Sections/Humans.Tickets/TicketsResource.es.resx b/src/Sections/Humans.Tickets/TicketsResource.es.resx index 91f03ea521..919df61eba 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.es.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.es.resx @@ -65,7 +65,7 @@ 3. Confirmar Transferir una entrada Tus solicitudes de transferencia - No se pudo preparar la transferencia: elige una de tus entradas y una persona destinataria válida que no seas tú. - Transferencia solicitada. Nuestro equipo de entradas la procesará y te avisará en breve. - Transferencia cancelada. + No se pudo preparar la transferencia: elige una de tus entradas y una persona destinataria válida que no seas tú. + Transferencia solicitada. Nuestro equipo de entradas la procesará y te avisará en breve. + Transferencia cancelada. diff --git a/src/Sections/Humans.Tickets/TicketsResource.fr.resx b/src/Sections/Humans.Tickets/TicketsResource.fr.resx index 545c13c017..89f3889b0a 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.fr.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.fr.resx @@ -65,7 +65,7 @@ 3. Confirmer Transférer un billet Vos demandes de transfert - Impossible de préparer ce transfert : choisissez l'un de vos billets et une personne destinataire valide autre que vous-même. - Transfert demandé. Notre équipe billetterie le traitera et vous tiendra informé sous peu. - Transfert annulé. + Impossible de préparer ce transfert : choisissez l'un de vos billets et une personne destinataire valide autre que vous-même. + Transfert demandé. Notre équipe billetterie le traitera et vous tiendra informé sous peu. + Transfert annulé. diff --git a/src/Sections/Humans.Tickets/TicketsResource.it.resx b/src/Sections/Humans.Tickets/TicketsResource.it.resx index 158eb319fd..9b54c6c8b8 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.it.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.it.resx @@ -65,7 +65,7 @@ 3. Conferma Trasferisci un biglietto Le tue richieste di trasferimento - Impossibile preparare il trasferimento: scegli uno dei tuoi biglietti e una persona destinataria valida diversa da te. - Trasferimento richiesto. Il nostro team biglietti lo elaborerà e ti farà sapere a breve. - Trasferimento annullato. + Impossibile preparare il trasferimento: scegli uno dei tuoi biglietti e una persona destinataria valida diversa da te. + Trasferimento richiesto. Il nostro team biglietti lo elaborerà e ti farà sapere a breve. + Trasferimento annullato. diff --git a/src/Sections/Humans.Tickets/TicketsResource.resx b/src/Sections/Humans.Tickets/TicketsResource.resx index a064c664ee..f549fa5c08 100644 --- a/src/Sections/Humans.Tickets/TicketsResource.resx +++ b/src/Sections/Humans.Tickets/TicketsResource.resx @@ -54,7 +54,7 @@ 3. Confirm Transfer a ticket Your transfer requests - Couldn't set up that transfer — choose one of your tickets and a valid recipient other than yourself. - Transfer requested. Our ticketing team will process it and let you know shortly. - Transfer cancelled. + Couldn't set up that transfer — choose one of your tickets and a valid recipient other than yourself. + Transfer requested. Our ticketing team will process it and let you know shortly. + Transfer cancelled. diff --git a/src/Sections/Humans.Users/Controllers/GuestAccountController.cs b/src/Sections/Humans.Users/Controllers/GuestAccountController.cs index 5d8174a3a0..27cce54d47 100644 --- a/src/Sections/Humans.Users/Controllers/GuestAccountController.cs +++ b/src/Sections/Humans.Users/Controllers/GuestAccountController.cs @@ -49,7 +49,7 @@ public async Task CommunicationPreferences(string? utoken) catch (Exception ex) { logger.LogError(ex, "Failed to load communication preferences"); - SetError(localizer["Profile_CommunicationPreferencesLoadFailed"].Value); + SetError(localizer["Users_Profile_CommunicationPreferencesLoadFailed"].Value); return RedirectToAction("Index", "Guest"); } } @@ -115,7 +115,7 @@ public async Task RequestDeletion() catch (Exception ex) { logger.LogError(ex, "Failed to process deletion request for user {UserId}", user.Id); - SetError(localizer["Guest_DeletionRequestFailed"].Value); + SetError(localizer["Users_Guest_DeletionRequestFailed"].Value); return RedirectToAction("Index", "Guest"); } } @@ -133,7 +133,7 @@ public async Task CancelDeletion() { SetError(string.Equals(result.ErrorKey, "NoDeletionPending", StringComparison.Ordinal) ? localizer["Profile_NoDeletionPending"].Value - : localizer["Guest_CancelDeletionFailed"].Value); + : localizer["Users_Guest_CancelDeletionFailed"].Value); return RedirectToAction("Index", "Guest"); } diff --git a/src/Sections/Humans.Users/Controllers/ProfileController.cs b/src/Sections/Humans.Users/Controllers/ProfileController.cs index 0359cd15c6..54ad6d3e55 100644 --- a/src/Sections/Humans.Users/Controllers/ProfileController.cs +++ b/src/Sections/Humans.Users/Controllers/ProfileController.cs @@ -841,7 +841,7 @@ public async Task CommunicationPreferences() catch (Exception ex) { logger.LogError(ex, "Failed to load communication preferences"); - SetError(localizer["Profile_CommunicationPreferencesLoadFailed"].Value); + SetError(localizer["Users_Profile_CommunicationPreferencesLoadFailed"].Value); return RedirectToAction(nameof(Me)); } } diff --git a/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs b/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs index 4eb19a9df5..e84cdfb4a2 100644 --- a/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs +++ b/src/Sections/Humans.Users/Controllers/ProfileEmailsController.cs @@ -135,7 +135,7 @@ private void SetAddedEmailFlash(string email, bool isConflict) { if (isConflict) { - SetInfo(localizer["Profile_EmailLinkedToAnotherAccount"].Value); + SetInfo(localizer["Users_Profile_EmailLinkedToAnotherAccount"].Value); return; } @@ -181,7 +181,7 @@ private IActionResult VerifyEmailSuccess(Guid userId, VerifyEmailResult result) ViewData["Success"] = true; ViewData["Message"] = string.Format( CultureInfo.CurrentCulture, - localizer["Profile_EmailVerifiedMergeRequested"].Value, + localizer["Users_Profile_EmailVerifiedMergeRequested"].Value, result.Email); return View("VerifyEmailResult"); } diff --git a/src/Sections/Humans.Users/Controllers/ProfileViewController.cs b/src/Sections/Humans.Users/Controllers/ProfileViewController.cs index e4a1b8009e..81b4da6ad0 100644 --- a/src/Sections/Humans.Users/Controllers/ProfileViewController.cs +++ b/src/Sections/Humans.Users/Controllers/ProfileViewController.cs @@ -299,7 +299,7 @@ public async Task SendMessage(Guid id, Guid? teamId, Cancellation if (!await commPrefService.AcceptsFacilitatedMessagesAsync(id, ct)) { - SetError(localizer["Profile_MessageOptedOut"].Value); + SetError(localizer["Users_Profile_MessageOptedOut"].Value); return RedirectToAction(nameof(ViewProfile), new { id }); } @@ -344,7 +344,7 @@ public async Task SendMessage(Guid id, SendMessageViewModel model if (!await commPrefService.AcceptsFacilitatedMessagesAsync(id, ct)) { - SetError(localizer["Profile_MessageOptedOut"].Value); + SetError(localizer["Users_Profile_MessageOptedOut"].Value); return RedirectToAction(nameof(ViewProfile), new { id }); } diff --git a/src/Sections/Humans.Users/Models/GuestDeletionRequestFlash.cs b/src/Sections/Humans.Users/Models/GuestDeletionRequestFlash.cs index e30720b2ff..ea9b17c37d 100644 --- a/src/Sections/Humans.Users/Models/GuestDeletionRequestFlash.cs +++ b/src/Sections/Humans.Users/Models/GuestDeletionRequestFlash.cs @@ -12,12 +12,12 @@ public static GuestDeletionRequestFlash From(DeletionRequestResult result) return new( true, - result.IsHeldForTicket ? "Guest_DeletionHeldForTicket" : "Guest_DeletionRequested", + result.IsHeldForTicket ? "Users_Guest_DeletionHeldForTicket" : "Users_Guest_DeletionRequested", result.EffectiveDeletionDate); } private static string ErrorResourceKeyFor(string? errorKey) => string.Equals(errorKey, "AlreadyPending", StringComparison.Ordinal) ? "Profile_DeletionAlreadyPending" - : "Guest_DeletionRequestFailed"; + : "Users_Guest_DeletionRequestFailed"; } diff --git a/src/Sections/Humans.Users/UsersResource.ca.resx b/src/Sections/Humans.Users/UsersResource.ca.resx index 2d40ffe43e..cb9cd7282b 100644 --- a/src/Sections/Humans.Users/UsersResource.ca.resx +++ b/src/Sections/Humans.Users/UsersResource.ca.resx @@ -1154,12 +1154,12 @@ <p>Una salutació,<br/>L’equip de Humans</p> {0} = user name - Aquesta persona ha decidit no rebre missatges. - No s'han pogut carregar les preferències de comunicació. - Aquest correu està vinculat a un altre compte. En verificar-lo se sol·licitarà una fusió de comptes. Consulta la safata d'entrada per a l'enllaç de verificació. - Correu verificat. S'ha enviat una sol·licitud de fusió per a la revisió administrativa. El correu {0} s'afegirà al teu compte quan s'aprovi.{0} = email address - No s'ha pogut processar la sol·licitud d'eliminació. Torna-ho a provar. - Sol·licitud d'eliminació registrada. El teu compte s'eliminarà definitivament el {0}.{0} = deletion date - Sol·licitud d'eliminació registrada. Com que tens entrades per a un esdeveniment proper, el teu compte s'eliminarà després del {0}.{0} = deletion date - No s'ha pogut cancel·lar la sol·licitud d'eliminació. Torna-ho a provar. + Aquesta persona ha decidit no rebre missatges. + No s'han pogut carregar les preferències de comunicació. + Aquest correu està vinculat a un altre compte. En verificar-lo se sol·licitarà una fusió de comptes. Consulta la safata d'entrada per a l'enllaç de verificació. + Correu verificat. S'ha enviat una sol·licitud de fusió per a la revisió administrativa. El correu {0} s'afegirà al teu compte quan s'aprovi.{0} = email address + No s'ha pogut processar la sol·licitud d'eliminació. Torna-ho a provar. + Sol·licitud d'eliminació registrada. El teu compte s'eliminarà definitivament el {0}.{0} = deletion date + Sol·licitud d'eliminació registrada. Com que tens entrades per a un esdeveniment proper, el teu compte s'eliminarà després del {0}.{0} = deletion date + No s'ha pogut cancel·lar la sol·licitud d'eliminació. Torna-ho a provar. diff --git a/src/Sections/Humans.Users/UsersResource.de.resx b/src/Sections/Humans.Users/UsersResource.de.resx index 213088e2c3..cc6b29c5aa 100644 --- a/src/Sections/Humans.Users/UsersResource.de.resx +++ b/src/Sections/Humans.Users/UsersResource.de.resx @@ -1154,12 +1154,12 @@ <p>Alles Gute,<br/>Das Humans-Team</p> {0} = user name - Diese Person hat den Empfang von Nachrichten abgelehnt. - Kommunikationseinstellungen konnten nicht geladen werden. - Diese E-Mail ist mit einem anderen Konto verknüpft. Die Verifizierung fordert eine Kontozusammenführung an. Prüfe deinen Posteingang auf den Bestätigungslink. - E-Mail verifiziert. Eine Zusammenführungsanfrage wurde zur Prüfung durch die Administration eingereicht. Die E-Mail-Adresse {0} wird nach der Genehmigung deinem Konto hinzugefügt.{0} = email address - Der Löschantrag konnte nicht verarbeitet werden. Bitte versuche es erneut. - Löschantrag erfasst. Dein Konto wird am {0} endgültig gelöscht.{0} = deletion date - Löschantrag erfasst. Da du Tickets für eine bevorstehende Veranstaltung hast, wird dein Konto nach dem {0} gelöscht.{0} = deletion date - Der Löschantrag konnte nicht storniert werden. Bitte versuche es erneut. + Diese Person hat den Empfang von Nachrichten abgelehnt. + Kommunikationseinstellungen konnten nicht geladen werden. + Diese E-Mail ist mit einem anderen Konto verknüpft. Die Verifizierung fordert eine Kontozusammenführung an. Prüfe deinen Posteingang auf den Bestätigungslink. + E-Mail verifiziert. Eine Zusammenführungsanfrage wurde zur Prüfung durch die Administration eingereicht. Die E-Mail-Adresse {0} wird nach der Genehmigung deinem Konto hinzugefügt.{0} = email address + Der Löschantrag konnte nicht verarbeitet werden. Bitte versuche es erneut. + Löschantrag erfasst. Dein Konto wird am {0} endgültig gelöscht.{0} = deletion date + Löschantrag erfasst. Da du Tickets für eine bevorstehende Veranstaltung hast, wird dein Konto nach dem {0} gelöscht.{0} = deletion date + Der Löschantrag konnte nicht storniert werden. Bitte versuche es erneut. diff --git a/src/Sections/Humans.Users/UsersResource.es.resx b/src/Sections/Humans.Users/UsersResource.es.resx index 82701b1ca8..e3f29cf6f5 100644 --- a/src/Sections/Humans.Users/UsersResource.es.resx +++ b/src/Sections/Humans.Users/UsersResource.es.resx @@ -1154,12 +1154,12 @@ <p>Un saludo,<br/>El equipo de Humans</p> {0} = user name - Esta persona ha decidido no recibir mensajes. - No se pudieron cargar las preferencias de comunicación. - Este correo está vinculado a otra cuenta. Al verificarlo se solicitará una fusión de cuentas. Consulta tu bandeja de entrada para ver el enlace de verificación. - Correo verificado. Se ha enviado una solicitud de fusión para revisión administrativa. El correo {0} se añadirá a tu cuenta cuando se apruebe.{0} = email address - No se pudo procesar la solicitud de eliminación. Inténtalo de nuevo. - Solicitud de eliminación registrada. Tu cuenta se eliminará permanentemente el {0}.{0} = deletion date - Solicitud de eliminación registrada. Como tienes entradas para un próximo evento, tu cuenta se eliminará después del {0}.{0} = deletion date - No se pudo cancelar la solicitud de eliminación. Inténtalo de nuevo. + Esta persona ha decidido no recibir mensajes. + No se pudieron cargar las preferencias de comunicación. + Este correo está vinculado a otra cuenta. Al verificarlo se solicitará una fusión de cuentas. Consulta tu bandeja de entrada para ver el enlace de verificación. + Correo verificado. Se ha enviado una solicitud de fusión para revisión administrativa. El correo {0} se añadirá a tu cuenta cuando se apruebe.{0} = email address + No se pudo procesar la solicitud de eliminación. Inténtalo de nuevo. + Solicitud de eliminación registrada. Tu cuenta se eliminará permanentemente el {0}.{0} = deletion date + Solicitud de eliminación registrada. Como tienes entradas para un próximo evento, tu cuenta se eliminará después del {0}.{0} = deletion date + No se pudo cancelar la solicitud de eliminación. Inténtalo de nuevo. diff --git a/src/Sections/Humans.Users/UsersResource.fr.resx b/src/Sections/Humans.Users/UsersResource.fr.resx index 9d3ba57f88..590bb3e231 100644 --- a/src/Sections/Humans.Users/UsersResource.fr.resx +++ b/src/Sections/Humans.Users/UsersResource.fr.resx @@ -1154,12 +1154,12 @@ <p>Cordialement,<br/>L'équipe Humans</p> {0} = user name - Cette personne a choisi de ne pas recevoir de messages. - Impossible de charger les préférences de communication. - Cette adresse e-mail est liée à un autre compte. Sa vérification demandera une fusion de comptes. Consultez votre boîte de réception pour le lien de vérification. - Adresse e-mail vérifiée. Une demande de fusion a été soumise à l'examen administratif. L'adresse e-mail {0} sera ajoutée à votre compte après approbation.{0} = email address - Impossible de traiter la demande de suppression. Veuillez réessayer. - Demande de suppression enregistrée. Votre compte sera définitivement supprimé le {0}.{0} = deletion date - Demande de suppression enregistrée. Comme vous avez des billets pour un prochain événement, votre compte sera supprimé après le {0}.{0} = deletion date - Impossible d'annuler la demande de suppression. Veuillez réessayer. + Cette personne a choisi de ne pas recevoir de messages. + Impossible de charger les préférences de communication. + Cette adresse e-mail est liée à un autre compte. Sa vérification demandera une fusion de comptes. Consultez votre boîte de réception pour le lien de vérification. + Adresse e-mail vérifiée. Une demande de fusion a été soumise à l'examen administratif. L'adresse e-mail {0} sera ajoutée à votre compte après approbation.{0} = email address + Impossible de traiter la demande de suppression. Veuillez réessayer. + Demande de suppression enregistrée. Votre compte sera définitivement supprimé le {0}.{0} = deletion date + Demande de suppression enregistrée. Comme vous avez des billets pour un prochain événement, votre compte sera supprimé après le {0}.{0} = deletion date + Impossible d'annuler la demande de suppression. Veuillez réessayer. diff --git a/src/Sections/Humans.Users/UsersResource.it.resx b/src/Sections/Humans.Users/UsersResource.it.resx index 59a4516c19..85ae9502ab 100644 --- a/src/Sections/Humans.Users/UsersResource.it.resx +++ b/src/Sections/Humans.Users/UsersResource.it.resx @@ -1154,12 +1154,12 @@ <p>Cordiali saluti,<br/>Il team di Humans</p> {0} = user name - Questa persona ha scelto di non ricevere messaggi. - Impossibile caricare le preferenze di comunicazione. - Questa email è collegata a un altro account. La verifica richiederà un'unione di account. Controlla la posta in arrivo per il link di verifica. - Email verificata. È stata inviata una richiesta di unione per la revisione amministrativa. L'email {0} verrà aggiunta al tuo account una volta approvata.{0} = email address - Impossibile elaborare la richiesta di eliminazione. Riprova. - Richiesta di eliminazione registrata. Il tuo account verrà eliminato definitivamente il {0}.{0} = deletion date - Richiesta di eliminazione registrata. Poiché hai biglietti per un prossimo evento, il tuo account verrà eliminato dopo il {0}.{0} = deletion date - Impossibile annullare la richiesta di eliminazione. Riprova. + Questa persona ha scelto di non ricevere messaggi. + Impossibile caricare le preferenze di comunicazione. + Questa email è collegata a un altro account. La verifica richiederà un'unione di account. Controlla la posta in arrivo per il link di verifica. + Email verificata. È stata inviata una richiesta di unione per la revisione amministrativa. L'email {0} verrà aggiunta al tuo account una volta approvata.{0} = email address + Impossibile elaborare la richiesta di eliminazione. Riprova. + Richiesta di eliminazione registrata. Il tuo account verrà eliminato definitivamente il {0}.{0} = deletion date + Richiesta di eliminazione registrata. Poiché hai biglietti per un prossimo evento, il tuo account verrà eliminato dopo il {0}.{0} = deletion date + Impossibile annullare la richiesta di eliminazione. Riprova. diff --git a/src/Sections/Humans.Users/UsersResource.resx b/src/Sections/Humans.Users/UsersResource.resx index e6e5b5fd10..52f62a8a61 100644 --- a/src/Sections/Humans.Users/UsersResource.resx +++ b/src/Sections/Humans.Users/UsersResource.resx @@ -549,12 +549,12 @@ <p>As requested, your Humans account has been permanently deleted. All your personal data has been removed from our systems.</p> <p>Thank you for being part of our community. If you ever wish to rejoin, you're welcome to create a new account.</p> <p>Best wishes,<br/>The Humans Team</p>{0} = user name - This human has opted out of receiving messages. - Failed to load communication preferences. - This email is linked to another account. Verifying it will request an account merge. Check your inbox for the verification link. - Email verified. A merge request has been submitted for admin review. The email {0} will be added to your account once approved.{0} = email address - Failed to process deletion request. Please try again. - Deletion request recorded. Your account will be permanently deleted on {0}.{0} = deletion date - Deletion request recorded. Because you have tickets for an upcoming event, your account will be deleted after {0}.{0} = deletion date - Failed to cancel deletion request. Please try again. + This human has opted out of receiving messages. + Failed to load communication preferences. + This email is linked to another account. Verifying it will request an account merge. Check your inbox for the verification link. + Email verified. A merge request has been submitted for admin review. The email {0} will be added to your account once approved.{0} = email address + Failed to process deletion request. Please try again. + Deletion request recorded. Your account will be permanently deleted on {0}.{0} = deletion date + Deletion request recorded. Because you have tickets for an upcoming event, your account will be deleted after {0}.{0} = deletion date + Failed to cancel deletion request. Please try again. diff --git a/tests/Humans.Containers.Tests/Services/ServiceImageTests.cs b/tests/Humans.Containers.Tests/Services/ServiceImageTests.cs index bef8a8414a..c118ae56db 100644 --- a/tests/Humans.Containers.Tests/Services/ServiceImageTests.cs +++ b/tests/Humans.Containers.Tests/Services/ServiceImageTests.cs @@ -108,7 +108,7 @@ public async Task CreateAsync_RejectsMoreThanFiveImages() NewImages: FakeImages(6)), ct: TestContext.Current.CancellationToken); await act.Should().ThrowAsync() - .WithMessage(Service.TooManyImagesError); + .WithMessage("*at most 5 images*"); } [HumansFact] @@ -123,7 +123,7 @@ public async Task UpdateAsync_RejectsWhenAddedImagesWouldExceedFive() NewImages: FakeImages(2)), actorUserId: Guid.NewGuid(), ct: TestContext.Current.CancellationToken); await act.Should().ThrowAsync() - .WithMessage(Service.TooManyImagesError); + .WithMessage("*at most 5 images*"); } [HumansFact] @@ -138,7 +138,7 @@ public async Task UpdateAsync_CountsTheLegacyImageAgainstTheCap() NewImages: FakeImages(1)), actorUserId: Guid.NewGuid(), ct: TestContext.Current.CancellationToken); await act.Should().ThrowAsync() - .WithMessage(Service.TooManyImagesError); + .WithMessage("*at most 5 images*"); } [HumansFact] @@ -234,7 +234,7 @@ public async Task CreateAsync_RejectsNameWithTokenSignificantCharacters(string n Description: null), ct: TestContext.Current.CancellationToken); await act.Should().ThrowAsync() - .WithMessage(Service.InvalidNameError); + .WithMessage("*must not contain*"); } [HumansFact] diff --git a/tests/Humans.GoogleIntegration.Tests/GoogleResourceRepositoryTests.cs b/tests/Humans.GoogleIntegration.Tests/GoogleResourceRepositoryTests.cs index 398aef02f7..94fb9688c3 100644 --- a/tests/Humans.GoogleIntegration.Tests/GoogleResourceRepositoryTests.cs +++ b/tests/Humans.GoogleIntegration.Tests/GoogleResourceRepositoryTests.cs @@ -46,7 +46,7 @@ public void Dispose() } [HumansFact] - public async Task GetActiveByTeamIdAsync_ExcludesInactive() + public async Task GetActiveByTeamIdAsync_OrdersByProvisionedAt_ExcludesInactive() { var teamId = Guid.NewGuid(); var older = Seed(teamId, "older", GoogleResourceType.DriveFolder, Instant.FromUtc(2026, 4, 20, 0, 0)); @@ -57,7 +57,7 @@ public async Task GetActiveByTeamIdAsync_ExcludesInactive() var rows = await _repository.GetActiveByTeamIdAsync(teamId, Xunit.TestContext.Current.CancellationToken); rows.Should().HaveCount(2); - rows.Select(r => r.Id).Should().BeEquivalentTo([older.Id, newer.Id]); + rows.Select(r => r.Id).Should().ContainInOrder(older.Id, newer.Id); } [HumansFact] diff --git a/tests/Humans.Users.Tests/Controllers/GuestAccountControllerTests.cs b/tests/Humans.Users.Tests/Controllers/GuestAccountControllerTests.cs index 0e393e247b..0678e01a61 100644 --- a/tests/Humans.Users.Tests/Controllers/GuestAccountControllerTests.cs +++ b/tests/Humans.Users.Tests/Controllers/GuestAccountControllerTests.cs @@ -39,7 +39,7 @@ private GuestAccountController BuildSut(User user) var value = key switch { "Profile_DeletionAlreadyPending" => "A deletion request is already pending.", - "Guest_DeletionRequested" => "Deletion request recorded. Your account will be permanently deleted on {0}.", + "Users_Guest_DeletionRequested" => "Deletion request recorded. Your account will be permanently deleted on {0}.", _ => key, }; return new LocalizedString(key, value); diff --git a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt index ca0c300197..cef9da4dfc 100644 --- a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt +++ b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt @@ -6,6 +6,8 @@ # When violations are FIXED, remove the corresponding line from this file. # When the test reports new violations, fix the code — do not add lines to silence it. +src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs:OrderBy#1 +src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs:OrderBy#2 src/Sections/Humans.Teams/Data/TeamRepository.cs:OrderBy#1 src/Sections/Humans.Teams/Data/TeamRepository.cs:ThenBy#1 src/Sections/Humans.Teams/Data/TeamRepository.cs:ThenBy#2 From 56e26bf8f9642344b0d5019916d8c4fadecf1026 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 21:26:38 +0000 Subject: [PATCH 28/29] Revert display-sort relocations into services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverted: 010e513b Move campaign tracking sort out of repository — sort landed in CampaignService, still below the controller boundary Reverted: 524f56c2 Move camp role definition sorting out of repository — sort landed in CampRoleService, same relocation Both repository sorts and their DisplaySortInControllers baseline lines are restored, so the debt stays recorded. Fixed: CampaignService.cs:577 display sort moved into service (Codex, via revert) Fixed: CampRoleService.cs:662 display sort moved into service (Codex, via revert) Review-round: 2 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012jUyNBoz1pjgWZmmTPryHr --- .../Humans.Campaigns/Data/CampaignRepository.cs | 3 ++- .../Humans.Campaigns/Data/ICampaignRepository.cs | 3 +-- .../Humans.Campaigns/Services/CampaignService.cs | 1 - .../Humans.Camps/Data/CampRepository.Roles.cs | 2 +- .../Humans.Camps/Services/CampRoleService.cs | 12 ++++-------- .../Services/CampaignServiceTests.cs | 12 ------------ .../Baselines/DisplaySortInControllers.baseline.txt | 3 +++ 7 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/Sections/Humans.Campaigns/Data/CampaignRepository.cs b/src/Sections/Humans.Campaigns/Data/CampaignRepository.cs index 255720e6dc..16ecaf84e8 100644 --- a/src/Sections/Humans.Campaigns/Data/CampaignRepository.cs +++ b/src/Sections/Humans.Campaigns/Data/CampaignRepository.cs @@ -61,7 +61,8 @@ public async Task> GetCodeTracking return await ctx.Campaigns .AsNoTracking() .Where(c => c.Status == CampaignStatus.Active || c.Status == CampaignStatus.Completed) - .Select(c => new CampaignCodeTrackingSummaryRow(c.Id, c.Title, c.CreatedAt)) + .OrderByDescending(c => c.CreatedAt) + .Select(c => new CampaignCodeTrackingSummaryRow(c.Id, c.Title)) .ToListAsync(ct); } diff --git a/src/Sections/Humans.Campaigns/Data/ICampaignRepository.cs b/src/Sections/Humans.Campaigns/Data/ICampaignRepository.cs index de69c5a240..f966ca0bf3 100644 --- a/src/Sections/Humans.Campaigns/Data/ICampaignRepository.cs +++ b/src/Sections/Humans.Campaigns/Data/ICampaignRepository.cs @@ -183,8 +183,7 @@ internal sealed record GrantWithSendContext( ///
internal sealed record CampaignCodeTrackingSummaryRow( Guid CampaignId, - string CampaignTitle, - Instant CreatedAt); + string CampaignTitle); /// /// One grant per row, used by . diff --git a/src/Sections/Humans.Campaigns/Services/CampaignService.cs b/src/Sections/Humans.Campaigns/Services/CampaignService.cs index 5c9d4954dc..ee90ebe63f 100644 --- a/src/Sections/Humans.Campaigns/Services/CampaignService.cs +++ b/src/Sections/Humans.Campaigns/Services/CampaignService.cs @@ -574,7 +574,6 @@ public async Task GetCodeTrackingAsync(CancellationTok .ToDictionary(g => g.Key, g => g.ToList()); var summaries = summaryRows - .OrderByDescending(s => s.CreatedAt) .Select(s => { grantsByCampaign.TryGetValue(s.CampaignId, out var grants); diff --git a/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs b/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs index a663cdb629..082449ef4e 100644 --- a/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs +++ b/src/Sections/Humans.Camps/Data/CampRepository.Roles.cs @@ -11,7 +11,7 @@ public async Task> ListDefinitionsAsync(bool i var query = ctx.CampRoleDefinitions.AsNoTracking().AsQueryable(); if (!includeDeactivated) query = query.Where(d => d.DeactivatedAt == null); - return await query.ToListAsync(ct); + return await query.OrderBy(d => d.SortOrder).ThenBy(d => d.Name).ToListAsync(ct); } public async Task GetDefinitionByIdAsync(Guid id, CancellationToken ct = default) diff --git a/src/Sections/Humans.Camps/Services/CampRoleService.cs b/src/Sections/Humans.Camps/Services/CampRoleService.cs index 5fa5575815..b7cdb5131a 100644 --- a/src/Sections/Humans.Camps/Services/CampRoleService.cs +++ b/src/Sections/Humans.Camps/Services/CampRoleService.cs @@ -23,7 +23,7 @@ internal sealed class CampRoleService( public async Task> ListDefinitionsAsync(bool includeDeactivated, CancellationToken ct = default) { - var definitions = OrderDefinitions(await repo.ListDefinitionsAsync(includeDeactivated, ct)); + var definitions = await repo.ListDefinitionsAsync(includeDeactivated, ct); return definitions.Select(CreateCampRoleDefinitionInfo).ToList(); } @@ -222,7 +222,7 @@ await auditLog.LogAsync( public async Task BuildPanelAsync(Guid campSeasonId, CancellationToken ct = default) { - var definitions = OrderDefinitions(await repo.ListDefinitionsAsync(includeDeactivated: false, ct)); + var definitions = await repo.ListDefinitionsAsync(includeDeactivated: false, ct); var assignments = await repo.GetAssignmentsForSeasonAsync(campSeasonId, ct); var memberUserIds = assignments.Select(a => a.CampMember.UserId).Distinct().ToList(); @@ -377,8 +377,8 @@ await auditLog.LogAsync( public async Task>> GetDirectoryRoleSummariesAsync(int year, CancellationToken ct = default) { - var definitions = OrderDefinitions(await repo.ListDefinitionsAsync(includeDeactivated: false, ct)); - if (!definitions.Any()) + var definitions = await repo.ListDefinitionsAsync(includeDeactivated: false, ct); + if (definitions.Count == 0) return new Dictionary>(); // Same shape as GetComplianceReportAsync but over all active definitions, @@ -657,10 +657,6 @@ async Task> ICampRoleSeeding.ListDefin return definitions.Select(d => new CampRoleDefinitionSeedInfo(d.Id, d.Slug, d.Name)).ToList(); } - private static IOrderedEnumerable OrderDefinitions( - IEnumerable definitions) => - definitions.OrderBy(d => d.SortOrder).ThenBy(d => d.Name, StringComparer.OrdinalIgnoreCase); - /// async Task ICampRoleSeeding.CreateDefinitionForSeedAsync( string name, string slug, string? description, int slotCount, int minimumRequired, diff --git a/tests/Humans.Campaigns.Tests/Services/CampaignServiceTests.cs b/tests/Humans.Campaigns.Tests/Services/CampaignServiceTests.cs index d59c5eed75..755dadb0f3 100644 --- a/tests/Humans.Campaigns.Tests/Services/CampaignServiceTests.cs +++ b/tests/Humans.Campaigns.Tests/Services/CampaignServiceTests.cs @@ -741,18 +741,6 @@ await CampaignsDb.CampaignGrants.AddAsync(new CampaignGrant grant.Code == "TRACK-CODE" && grant.RedeemedAt == redeemedAt && grant.LatestEmailStatus == "Sent"); } - [HumansFact] - public async Task GetCodeTrackingAsync_ordersCampaignsNewestFirst() - { - var older = await SeedActiveCampaignWithCodesAsync(["OLDER"]); - Clock.AdvanceHours(1); - var newer = await SeedActiveCampaignWithCodesAsync(["NEWER"]); - - var tracking = await _service.GetCodeTrackingAsync(Xunit.TestContext.Current.CancellationToken); - - tracking.Campaigns.Select(c => c.CampaignId).Should().Equal(newer.Id, older.Id); - } - [HumansFact] public async Task EraseForUserAsync_DeletesOnlyThatUsersGrants() { diff --git a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt index cef9da4dfc..99232c8336 100644 --- a/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt +++ b/tests/Humans.Web.Tests/Architecture/Baselines/DisplaySortInControllers.baseline.txt @@ -6,6 +6,8 @@ # When violations are FIXED, remove the corresponding line from this file. # When the test reports new violations, fix the code — do not add lines to silence it. +src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:OrderBy#1 +src/Sections/Humans.Camps/Data/CampRepository.Roles.cs:ThenBy#1 src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs:OrderBy#1 src/Sections/Humans.GoogleIntegration/Data/GoogleResourceRepository.cs:OrderBy#2 src/Sections/Humans.Teams/Data/TeamRepository.cs:OrderBy#1 @@ -29,3 +31,4 @@ src/Sections/Humans.Tickets/Data/TicketRepository.cs:OrderByDescending#6 src/Sections/Humans.Tickets/Data/TicketRepository.cs:OrderByDescending#7 src/Sections/Humans.Tickets/Data/TicketRepository.cs:OrderByDescending#8 src/Sections/Humans.Tickets/Data/TicketRepository.cs:OrderByDescending#9 +src/Sections/Humans.Campaigns/Data/CampaignRepository.cs:OrderByDescending#1 From a88a5c97888a5fa37d633a4bfdca69fc5bba2082 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 21:44:39 +0000 Subject: [PATCH 29/29] Restore EVENTS-7 debt row, narrowed to form labels The submit forms'