Skip to content

Dev - #377

Merged
SteveTeece merged 87 commits into
masterfrom
dev
Sep 3, 2026
Merged

Dev#377
SteveTeece merged 87 commits into
masterfrom
dev

Conversation

@SteveTeece

Copy link
Copy Markdown
Owner

No description provided.

SteveTeece and others added 30 commits August 27, 2026 21:51
Feature 027 introduces per-language resource files (Australian English
baseline) so every screen, report, menu, tile, exception message and
user-facing enum value reads its text via IStringLocalizer, plus a
persisted, OS-aware Settings.LanguageCode selection.

Adds the full spec-kit artifact set:
- spec.md - 3 prioritized user stories, FR-001..FR-024, SC-001..SC-010
- plan.md, research.md - IStringLocalizer + ~12 area .resx markers,
  missing-key logging decorator, MoneyFormatter (fixed AUD), startup
  culture ladder (explicit -> OS language -> en-AU)
- data-model.md - Settings.LanguageCode column + migration outline
- contracts/ - localization interfaces + resource-key catalog/guards
- quickstart.md, checklists/requirements.md
- tasks.md - 64 tasks (T001-T064) in 6 wave-grouped phases

Bumps .specify/feature.json to specs/027-localization-support.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 1 (Setup):
- Add Microsoft.Extensions.Localization 10.0.10 to Directory.Packages.props
- Scaffold StageFright.Localization.Tests guard-test project (refs Core/UI/Reports)
- Add version-less PackageReference + NeutralLanguage=en-AU to Core/Reports/UI csproj

Phase 2 (Foundational):
- 12 area resource marker classes + empty neutral .resx: NavigationResource,
  ValidationResource, EnumsResource (Core); ReportsResource (Reports);
  Shared/Dashboard/Members/Rehearsals/Events/Finance/Settings/SetupResource (UI)
- ILocalizer facade + Localizer impl (Get/Get-with-args/Plural/Enum), with
  named-placeholder substitution by order of first appearance (FR-010)
- MissingKeyLoggingLocalizerFactory: IStringLocalizerFactory decorator that logs
  a Warning and falls back to neutral on a missing key (FR-008/FR-009)
- EnumLocalizationExtensions.LocalizeEnum: Enum_<Type>_<Member> lookup via a
  factory holder set once at startup (FR-024)
- MoneyFormatter.Format/FormatWithCode: fixed "$"/"AUD" symbol, culture-driven
  separators/grouping (FR-015)
- Guard-test scanning infrastructure (LocalizerKeyUsageScanner, ResxKeyScanner,
  PlaceholderTokenScanner, UserFacingEnumScanner) — no assertions yet
- LocalizedTestContext bUnit base wiring AddLocalization() + LocalizeEnum
- DI wiring in MauiProgram: AddLocalization(), decorated IStringLocalizerFactory,
  AddScoped<ILocalizer, Localizer>(), EnumLocalizationExtensions.UseFactory at startup

Full solution build: 0 warnings, 0 errors. Full test suite: 1693 existing tests
pass unchanged. User stories 1-3 and Polish (T015-T064) remain undone.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 3 / User Story 1 — the extraction pattern proven end-to-end on the
navigation shell plus the complete Members module. Same en-AU wording, now
sourced from IStringLocalizer<T> resources.

Resources (T017-T021):
- NavigationResource / MembersResource / ValidationResource / EnumsResource /
  SharedResource .resx populated with verbatim en-AU entries (labels, headings,
  buttons, placeholders, aria-label/alt/title, plural pair, enum display text).

Call sites (T022-T028):
- ShellLayout, ThemeProvider (no-op — no user text), MemberMenuItemProvider,
  MemberValidationService, MemberList/MemberDetail/MemberForm (.razor+.razor.cs),
  MembersTile, MembersDashboardTileProvider read text via IStringLocalizer.
- MemberStatus + Theme render through LocalizeEnum() (FR-024); fee amounts via
  MoneyFormatter.Format (FR-015); parameterised strings via ILocalizer helper
  methods in code-behind.
- Completed the T011 gap: @using StageFright.Core.Localization in _Imports.razor.
- DI unchanged — all three localizer-taking registrations resolve from
  AddLocalization() + the decorating factory.

Tests (T015-T016):
- New StageFright.Localization.Tests US1 guard suite: baseline completeness
  (+plural pairing), MemberStatus/Theme enum-key coverage, residual-literal scan
  over the US1 slice, no-raw-enum-render, no-"C"-currency-format, missing-key
  warning + neutral fallback.
- New FR-018 bUnit tests assert US1 text resolves via resource keys / LocalizeEnum.
- 5 existing bUnit classes moved to LocalizedTestContext; it now also registers
  ILocalizer. StubStringLocalizer / RealStringLocalizer test helpers added for
  the constructor-injection sites.

Full build clean; 1706 tests pass (Localization 6, Reports 178, Core 597,
UI 587, Data 152, Integration 186). US2/US3/Polish not started.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move every user-facing literal in Pages/Rehearsals/** into
RehearsalsResource.resx (en-AU verbatim) read through IStringLocalizer
at render time: RehearsalList, RehearsalForm and AttendanceGrid .razor
+ .razor.cs, including aria-label/placeholder text (FR-001). The
attendance fee now formats through MoneyFormatter, not ToString("C")
(FR-015). Parameterised strings (search term, dates, member names,
attendance rate) resolve via ILocalizer.Get in code-behind helpers.

RehearsalFormModel.Date drops its dead DataAnnotations ErrorMessage
literal (RequiredAttribute never fires on a non-nullable DateTime).
Three catch blocks stop surfacing raw ex.Message, matching the US1
MemberForm precedent.

Tests: new Rehearsals-scoped guard suite (baseline completeness,
residual-literal incl. accessibility attrs, no-"C"-currency) as an
interim stand-in for the still-pending repo-wide T029/T030; new
FR-018 render tests; RehearsalListTests + AttendanceGridTests flipped
to LocalizedTestContext (resx values byte-identical to prior copy).

Full rebuild clean (0 warnings). 1714 tests pass (+8 from the US1
baseline). 29/64 tasks; US2 continues with T029/T030 + T031/T033-T045.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Dashboard page chrome, the six remaining dashboard tile bodies (Events,
Rehearsals, AttendanceTrend, Finance, CashFlow, OutstandingBalances) and
their tile providers now read every user-facing string from
DashboardResource / EventsResource / FinanceResource / RehearsalsResource
/ SharedResource. Chart dataset legend labels localized; tile fee/balance
amounts moved from .ToString("C") to MoneyFormatter (FR-015). Tile
providers ctor-inject IStringLocalizer<T> (matching MembersDashboardTile
provider). bUnit tile/dashboard test classes flipped to
LocalizedTestContext; provider-test + V8 integration ctors updated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…eys (US2, T038 + T042 groundwork)

All six navigation menu providers (Dashboard, Rehearsals, Events, Finance,
Settings, Reports) ctor-inject IStringLocalizer<NavigationResource> and
source Title/ShortLabel/sub-item labels from NavigationResource.resx
(matching MemberMenuItemProvider). EnumsResource.resx gains
Enum_<Type>_<Member> keys for FeeType, PaymentMethod, PaymentType,
AccountType, TaxCode, ReconciliationStatus and JournalEntryType (call-site
LocalizeEnum swaps land with each module + T042). Core.Tests gains a
RealStringLocalizer fixture; the two affected menu-provider test ctors updated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AddAccountForm, BorderedListBox, OpeningBalanceEntryForm,
ReactivationForgivenessDialog, ReportViewer and TileRenderer now read all
user-facing text (labels, headings, buttons, placeholders, aria-labels,
status/error messages, plural button labels) from SharedResource.resx.
AccountType options and account-type column render via LocalizeEnum;
forgiveness fee amounts and report money via MoneyFormatter; parameterised
strings resolved in code-behind helpers. SubmitButtonText / EmptyText
params made nullable with a localized fallback. 16 bUnit test classes that
render these components flipped to LocalizedTestContext. Full suite green
(1714).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
All 10 Events/AGM screens (EventList, EventForm, EventDetail,
ParticipationGrid, AgmList, AgmDetail, ScheduleAgm, RecordAgm,
RecordSpecialElection, AgmAttendanceGrid) plus AgmAttendanceReportPrinter
read every user-facing string, aria-label, badge, status text, validation
and print message from EventsResource.resx / SharedResource. Parameterised
strings via ILocalizer code-behind helpers; EventFormModel data-annotation
messages dropped to bare [Required] (dead on non-nullable value types,
matching US1). 8 Events bUnit test classes flipped to LocalizedTestContext.
Full suite green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SettingsPage tab titles/error-boundary text and the General, Sales Tax,
Committee, Event Types and Backup & Restore tabs read all user-facing
strings, labels, help text, badges, aria-labels, status/error/success
messages and validation from SettingsResource.resx / SharedResource. Theme
label via LocalizeEnum; TaxCode options via LocalizeEnum (value stays the
invariant token); audit-retention option pluralised via ILocalizer.Plural;
fee input adornments fixed to "$" per FR-015 (was culture CurrencySymbol).
Language picker row deliberately left for US3/T056. 5 Settings bUnit test
classes flipped to LocalizedTestContext. Full suite green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SetupWizard shell + all 9 tab components (GeneralAppearance, MembershipFees,
ThemeSelection, SalesTax, SampleData, ChartOfAccounts, OpeningBalances,
Committee, Review) read every user-facing string, label, help text,
placeholder, aria-label, badge and validation/error message from
SetupResource.resx. Review money via MoneyFormatter; Theme + TaxCode via
LocalizeEnum (wizard's Light/Dark <select> keeps invariant option values
per spec 017 FR-022); audit-retention & review lines pluralised via
ILocalizer.Plural; fee adornments fixed to "$". Language step deliberately
left for US3/T057. 5 Setup tab bUnit test classes flipped to
LocalizedTestContext. Full suite green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
StartupError.razor (database recovery page) and ReportsPage.razor read
their headings, body text, buttons, prompts and error messages from
SharedResource.resx. MainPage.xaml / App.xaml carry no user-facing text.
Remaining Pages/** (Finance) is T034.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Move every user-facing literal in the 13 Finance pages + code-behinds
into FinanceResource.resx (en-AU baseline). Fee/payment/account/tax/
reconciliation-status enums render via LocalizeEnum; every displayed
amount goes through MoneyFormatter (FR-015) instead of ToString("C") /
FormatString="{0:C}". aria-labels, placeholders and the embedded-link
"no accounts configured" warnings are localized too (FR-001).

Nine Finance bUnit test classes flipped to LocalizedTestContext so
IStringLocalizer<FinanceResource> resolves; neutral values are
byte-identical to the prior hardcoded copy so assertions stay green.

Full solution build clean; full suite 1714 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… groundwork)

The US1-deferred piece of T041: AgeCalculationService.ValidateDateOfBirth
now sources its three user-facing messages from ValidationResource via the
ILocalizer facade (named {Age}/{MaxYears}/{MinimumAge} placeholders), so the
service is DI-constructed with ILocalizer.

Call-site fan-out handled: MemberList/MemberDetail switch from `new()` to an
injected AgeCalculationService (registered in LocalizedTestContext for bUnit);
Core/Integration/Reports test projects each gain a RealLocalizer fixture and
pass it to the ~12 `new AgeCalculationService(...)` sites.

The remaining T041 work — service-thrown ValidationException/DomainValidation-
Exception message text across ~18 Core services — is not in this commit.

Full solution build clean; full suite 1714 passing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Route every report provider's user-facing text through the ILocalizer
facade against a new populated ReportsResource.resx (~120 en-AU entries,
transcribed verbatim per FR-004): report names, filter labels,
titles/subtitles (named-placeholder formatted), section headings, column
headers, subtotal/total + inline row labels, and the GL-imbalance
exception message. Enum-valued cells (MemberStatus, ReconciliationStatus)
resolve via ILocalizer.Enum (FR-024).

Filter option VALUES stay culture-invariant tokens (Options, unchanged —
still compared in GenerateAsync and persisted); the localised option
LABEL is carried on a new parallel ReportFilterDefinition.OptionLabels
list, rendered by ReportViewer with fallback to the token.

Report amounts keep decimal.ToString("F2") — providers never used "C",
and adding a $ symbol/grouping would break SC-002 render parity. Report
renderers (T040) and service exception text (T041) remain.

T030: ReportsResourceLabelTests (names/headers/section+total labels
resolve via ReportsResource, no raw-key/blank leak — FR-006) and
PluginTextNonEnglishCultureTests (TestPlugin English report + tile text
renders unchanged under fr-FR, run on a dedicated thread so the culture
never leaks into parallel tests — FR-020).

Build 0 warnings / 0 errors; full suite 1733 pass / 0 fail
(Reports 178->195, Integration 186->188).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rency sweeps (T042, T044)

T040: all 6 PDF report renderers now source their page chrome from
ReportsResource.resx (~21 en-AU keys, verbatim per FR-004):
- PdfReportRenderer: "Generated: {DateTime} UTC", footer "Page"/"of"
- AttendanceRollPdfRenderer: title, "Rehearsal: {Date} at {Time}", Name/Present/Pd headers
- AgmResultsPdfRenderer: title, meeting/attendance lines, "Elected Positions",
  "No positions recorded.", bold "{Label}: " position prefix, "General Committee Member:"
- CheckboxSheetPdfBuilder: shared "Name" column + footer; gains an ILocalizer param
  and a nameColumnHeader arg threaded to BuildMemberTable
- Event/AgmAttendanceSheetPdfRenderer: titles, date lines, Participated/Attended headers
The 5 concrete renderers ctor-inject ILocalizer (resolves from the existing
AddScoped<ILocalizer, Localizer>() - no MauiProgram change). CsvReportExporter has
no user-facing literals and is untouched. 8 direct test-construction sites now pass
RealLocalizer.Instance; ReportsResourceLabelTests gains 22 chrome-key + placeholder
assertions (FR-006).

T042: verified the user-facing enum sweep is complete - EnumsResource.resx holds
every Enum_<Type>_<Member> key for all 9 user-facing enums. Fixed the one residual
raw display site (the item deferred from US1): MemberDetail Fee Payment History
FeeType column now uses fee.FeeType.LocalizeEnum().

T044: verified zero decimal.ToString("C")/{0:C}/FormatString display sites remain in
UI or Reports. The one user-facing residual - BankReconciliationService.cs:153
ReconciliationException message - is exception-message text owned by T041.

Full solution build: 0 warnings / 0 errors. Full test suite: 1755 passed / 0 failed
(Reports 217, up from 195).

US2 remaining: T041 (user-facing exception Message text across ~18 Core services -
high blast radius, own session), T045 (DI recheck after T041's ctor changes),
T029 (repo-wide guard suite - blocked until T041).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… part 1)

Route user-facing ValidationException / DataIntegrityException / ImportException
Message text through ValidationResource for the eight non-Finance Core services:
AgmService, EventService, EventTypeService, CommitteeOfficeHolderTypeService,
AttendanceService, SettingsService, SetupService, BackupService.

- 34 new en-AU keys in ValidationResource.resx (verbatim wording, FR-004);
  named-placeholder keys for {Year}/{SchemaVersion}/{SupportedMajorVersion}/{EntityType}.
- Each service takes the ILocalizer facade via a required ctor param (matches the
  AgeCalculationService T041-groundwork precedent; only path that substitutes named
  tokens). All eight are plain AddScoped<I,Impl> registrations so DI auto-resolves
  the new param -- no MauiProgram change.
- BackupService DeserializeAndValidate / ValidateVersion / ValidateCompleteness
  change from private static to instance to read _localizer.
- EntityNotFoundException built-in message and DataAccessException wrappers stay
  English (diagnostic per FR-007 -- UI never shows them verbatim).
- 20 test call-sites updated to pass RealLocalizer.Instance; new RealLocalizer
  fixture added to StageFright.Data.Tests.
- Interim guard Us2ExceptionMessageGuardTests (baseline completeness + no residual
  literal) scoped to the eight files.

Full build 0 warnings / 0 errors. Full suite 1757 pass.

Remaining for US2: T041 Finance services (incl. BankReconciliationService {Difference:C}),
T045 DI recheck, T029 repo-wide guard suite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…de guard (T041, T029, T045)

T041 (Finance half): route user-facing ValidationException / ReconciliationException
Message text through ValidationResource for all 9 Finance services — AccountService,
BankReconciliationService, ExpensePaymentService, PaymentService, FeeService,
GeneralJournalService, IncomeEntryService, BankDepositService, OpeningBalanceService.

- 39 new verbatim en-AU ValidationResource keys (FR-004) + reuse of
  Validation_Settings_NotConfigured; named-token keys for {Name} (Account duplicate),
  {Date} (statement date), {Difference}/{Zero} (reconciliation finalise).
- BankReconciliationService finalise message: the {workspace.Difference:C} + literal
  "$0.00" (the T044-flagged C-format residual) now routes both amounts through
  MoneyFormatter.Format — byte-identical under en-AU, fixed-AUD under any culture (FR-015).
- Each service takes the ILocalizer facade via a required ctor param (matches the
  non-Finance T041 precedent). All 9 are plain AddScoped<I,Impl> so DI auto-resolves
  the new param — no MauiProgram change (T045).
- ~30 test ctor sites pass RealLocalizer.Instance (Core.Tests, Data.Tests,
  Integration.Tests). EntityNotFoundException / GLBalanceException wrappers stay
  English (diagnostic per FR-007).
- Us2ExceptionMessageGuardTests extended to cover the Finance services.

T029: new repo-wide Us2LocalizationGuardTests (8 facts) — baseline key completeness
across the US1+US2 surface, Enum_<Type>_<Member> coverage for all 9 user-facing enums
+ no-raw-enum-render, residual-literal scan over the UI display surface, orphan-satellite
keys, plural-pair + placeholder parity, cross-culture token parity, and no-"C"-currency
repo-wide. Report-provider filter option-VALUE tokens (guarded by ReportsResourceLabelTests,
T030) and DataAnnotations ErrorMessage / ValidationResult literals (compile-time constant,
resolved in T060) are documented exclusions.

resource-key-catalog.md §3 updated to reflect the implemented guard layout and exclusions.

Full rebuild 0 warnings / 0 errors. Full suite 1765 pass (Localization 19, Reports 217,
Core 597, UI 592, Data 152, Integration 188). US2 (Phase 4) complete — 45/64 tasks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-T059)

Adds the language-selection slice on top of the US1/US2 extraction:

- Settings.LanguageCode (nullable, presentation-only) + hand-written
  EF migration AddLanguageCodeToSettings (dotnet ef can't build the MAUI
  startup project here); SchemaVersion left at 1.1.0 (unchanged for the
  last ~6 settings migrations; asserted verbatim by backup tests).
- ILanguageProvider FR-023 startup ladder (explicit choice -> OS display
  language by exact then parent-language match -> en-AU), wired in
  MauiProgram to set the process culture before the first Blazor render;
  ISystemCultureProvider seam (SystemCultureProvider in the App layer).
- SupportedLanguagesCatalog: runtime discovery of shipped resource
  cultures by scanning satellite folders for the StageFright assemblies;
  qps-* pseudo-locales excluded; endonyms from CultureInfo.NativeName.
- Settings General tab + Setup Wizard language pickers (endonym <select>,
  active-marked, inline restart notice at the point of change - FR-021);
  choice flows through SetupRequest -> Settings at first-run Finish.
- CultureProvider cascading seam wrapping ShellLayout (parallel to
  ThemeProvider; no v1 behaviour change) - named CultureProvider to
  avoid colliding with the Core LanguageProvider service.
- qps-ploc test pseudo-locale: scripts/generate-pseudo-locale.py + 12
  regenerable <Marker>.qps-ploc.resx fixtures (bracketed/accented,
  {tokens} preserved, 3 keys omitted to exercise per-key fallback).
- Tests (+40): LanguageProvider ladder, catalog runtime discovery +
  qps-* exclusion, migration round-trip, V21 startup/SC-006 no-data-
  change, qps-ploc e2e, language-picker bUnit. LocalizedTestContext
  registers the new language services.

Full rebuild: 0 warnings / 0 errors. Full suite: 1805 pass, 0 fail
(Localization 23, Core 620, Reports 217, UI 599, Data 154, Integration 192).
Polish (T060-T064) remains.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…cs (T060-T064)

T060: fold the interim per-slice residual-literal scans into one repo-wide guard
(Us2LocalizationGuardTests.Should_HaveNoUserFacingLiteral_When_AppSurfaceScanned)
covering all of StageFright.UI + every user-facing exception Message literal in
StageFright.Core. The widened scan flagged App.razor (project root, outside the old
4-dir scope): localized its error-boundary + not-found chrome to 6 new Shared_App_*
keys (neutral + qps-ploc). DataAnnotations ErrorMessage args are frozen as the one
documented carve-out.

T061: docs/localization/adding-a-language.md — translator/maintainer guide (FR-022, SC-009).
T062: CLAUDE.md gains a Localization section + module-list entry + money-column data-grid
note; resource-key-catalog.md updated to past tense; spec 007 account-balance contract
gets a forward-note (its {0:C} columns moved to MoneyFormatter).
T063/T064: full rebuild 0 warn / 0 err; full suite 1805 pass (Localization 23).

Spec 027 status: completed (64/64 tasks).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Drop-in en-US satellite for all 12 area .resx files, translated from the
en-AU neutral baseline. The runtime SupportedLanguagesCatalog discovers it
automatically — no code change — so it now appears in Settings ▸ General
and the Setup Wizard language step.

Divergence from en-AU is spelling + vocabulary only (the two dialects are
otherwise identical across this corpus):
- organisation → organization  (Settings/Setup labels, Tax Summary subtitle)
- finalise / finalised → finalize / finalized  (Finance button, reconciliation
  validation messages, ReconciliationStatus enum label)
- cheque → check  (PaymentMethod enum label)
- cancelled → canceled  (report-generation message)
- "financial year" → "fiscal year"  (Settings + opening-balances wizard copy)

Keys, {named} placeholders and <comment> docs are byte-identical to neutral,
so the Us2 orphan-key / placeholder-parity guards pass.

Also make three previously culture-fragile bUnit assertions resolve their
expected text through the same IStringLocalizer the component uses, so they
hold under a US UI culture (GitHub CI runs windows-latest = en-US):
- ReconciliationWorkspaceTests: assert ReconciliationStatus.Finalised.LocalizeEnum()
- SetupWizardTests: assert the localized "Organisation Settings" tab title
- refresh the now-stale "v1 ships only en-AU" note in SupportedLanguagesCatalog

Full suite green under both the default culture and -culture en-US
(UI 599, Core 620, Data 154, Reports 217, Integration 192, Localization 23).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full French translation of all 12 area .resx files, dropped in beside the
en-AU neutral baseline. SupportedLanguagesCatalog discovers fr-FR at runtime —
no code change — so "français (France)" now appears in Settings ▸ General and
the Setup Wizard language step, and an fr / fr-FR operating system resolves to
it on first run. Selecting it renders the whole app in French with French
number/date formatting; the currency stays "$" / "AUD" (MoneyFormatter).

- ~1,000 <value> strings translated; keys, {named} placeholders, _One/_Other
  plural pairs and <comment> docs are byte-identical to neutral, so the Us2
  orphan-key / placeholder-parity / plural-pairing guards pass.
- AGM → "AGA" (assemblée générale annuelle); accounting terms use standard
  French (plan comptable, grand livre, bilan, compte de résultat, rapprochement
  bancaire, produits/charges, capitaux propres, écriture de journal…).
- Embedded-HTML values keep their <a href> markup; significant leading/trailing
  spaces preserved.

No test changes needed: the only test that deliberately runs under fr-FR
(PluginTextNonEnglishCultureTests) still passes, and every other test runs
under the machine culture (en-GB locally / en-US on CI) where the fr-FR
satellite is not loaded.

Full suite green under both the default culture and -culture en-US
(Core 620, Data 154, Reports 217, UI 599, Integration 192, Localization 23).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…, and tasks

Feature 028 readies the finance module for community/amateur-theatre
groups outside Australia, judged against universal double-entry and
bookkeeping good practice (not formal IFRS/GAAP). Sourced from GitHub
issue #341 and sub-issues #342-#352.

Adds the full spec-kit artifact set:
- spec.md - 10 prioritized user stories (currency, regional number
  entry, statement consistency, basis-of-accounting disclosure, bank
  reconciliation, prior-year period locks, financial-year start,
  audit-history retention, written policies, sales-tax i18n spike),
  FR-001..FR-033, SC-001..SC-013
- plan.md, research.md - configurable org currency replacing hard-coded
  AUD, culture-safe money parsing, period-lock service, report
  integrity checks + disclosures
- data-model.md - currency/period-lock/policy fields + migration outline
- contracts/ - currency-formatting, period-lock, reports, and
  settings-and-setup contracts
- checklists/requirements.md
- tasks.md - 94 tasks (T001-T094) across 13 phases

Bumps .specify/feature.json to specs/028-international-accounting-standards.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…008)

Phase 1 baseline confirmed green (1805 tests). Phase 2 lays the shared
foundation every user-story phase builds on — no user-visible behaviour
change yet; an AUD dataset formats byte-identically.

- SupportedCurrency record + CurrencyCatalog (curated ISO 4217 seed set:
  AUD/USD/EUR/GBP/NZD/CAD 2-digit, JPY 0-digit, KWD/BHD 3-digit) with
  case-insensitive TryGet (false + Default, no throw) and Get (throws
  ValidationException on an unknown code). Mirrors SupportedLanguagesCatalog.
- MoneyFormatter gains Configure(SupportedCurrency); Format/FormatWithCode
  now use the configured symbol/code and MinorUnitDigits while grouping and
  placement still follow CultureInfo.CurrentCulture. Falls back to
  CurrencyCatalog.Default (AUD/$/2) before Configure is called.
- MauiProgram configures MoneyFormatter for Settings.CurrencyCode right
  after the display culture is applied; failure leaves it on AUD default.
- Settings entity + EF config: add CurrencyCode ("AUD"),
  FinancialYearStartDay (1), ClosedThroughDate (null); AuditRetentionYears
  default 1 -> 5 (range 1-7 unchanged).
- Migration AddInternationalAccountingSettings: adds the three columns
  (NOT NULL DEFAULT backfills the existing row), alters only the
  AuditRetentionYears column default (no UpdateData, so a configured value
  is preserved).

Build 0W/0E; all 1805 tests green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…y display (T009-T028)

Phase 3 / User Story 1 (spec 028). Every screen, report, PDF and CSV amount now
renders in the organisation's configured currency (Settings.CurrencyCode, ISO 4217,
default AUD) with the right symbol and minor-unit precision; grouping/placement still
follow the active culture. An AUD dataset is byte-identical to the pre-028 output.

- TaxCalculator.SplitInclusive: optional minorUnitDigits (default 2); all callers pass
  CurrencyCatalog.Get(settings.CurrencyCode).MinorUnitDigits.
- First-run setup: mandatory #setup-currency picker on GeneralAppearanceTab bound to
  SetupFormModel.CurrencyCode; SetupService validates it against CurrencyCatalog
  (Validation_Setup_CurrencyUnknown) and persists the normalised code.
- SettingsService.SaveAsync rejects a post-setup currency change
  (Validation_Settings_CurrencyImmutable) — currency is fixed after setup.
- All StageFright.Reports providers + JournalEntryPage totals route money through
  MoneyFormatter.Format instead of ToString("F2")/"N2".
- New resource keys (neutral + en-US + fr-FR): Validation_Setup_CurrencyUnknown,
  Validation_Settings_CurrencyImmutable, Setup_General_CurrencyLabel/Help.
- Tests: CurrencyCatalogTests, MoneyFormatterTests, TaxCalculator 0/2/3-digit theory,
  Setup/Settings currency cases, CurrencyPickerTests (bUnit), AudZeroDriftTests +
  V28_CurrencyConfigurationTests (integration acceptance). MoneyFormatterState
  collection serialises MoneyFormatter.Configure-sensitive integration tests.
- Updated ~30 pre-existing report/UI/integration assertions to MoneyFormatter.Format(...).
- Docs: CLAUDE.md Localization note + spec 027 superseding notes for the fixed-$/AUD prose.

Full rebuild: 0W/0E. Tests: 1868 pass (was 1805).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… inputs (T029-T035)

Phase 4 / User Story 2 (spec 028). The manual journal and opening-balance forms
hand-rolled decimal.TryParse(value, NumberStyles.Number, CultureInfo.CurrentCulture)
on the value of an <input type="number"> — which the browser always serialises
invariant ("1.50"). Under fr-FR / de-DE the period was read as a thousands
separator and 1.50 posted to the ledger as 150 (FR-007…FR-009 / live data bug).

- New MoneyInput.Parse(string?): invariant decimal.TryParse with
  NumberStyles.AllowDecimalPoint | AllowLeadingSign; 0m for null/blank/unparseable.
  The single shared money-entry parse helper.
- JournalEntryPage.ParseAmount and OpeningBalanceEntryForm.SetAmount now delegate
  to MoneyInput.Parse; removed the CurrentCulture parse and the now-unused
  System.Globalization using from both.
- Tests: MoneyInputTests (unit, fr-FR/de-DE/en-AU), JournalEntryPageLocaleTests +
  OpeningBalanceEntryFormLocaleTests (bUnit — entered amount stored exactly),
  MoneyInputGuardTests (repo-wide: no UI money field parses with CurrentCulture;
  both handlers route through MoneyInput.Parse), V28_LocaleSafeMoneyEntryTests
  (integration acceptance — real journal/opening-balance services over SQLite,
  fr-FR/de-DE input stored exact to the cent and identical to en-AU).

Build: 0W/0E. Tests: 1895 pass (was 1868, +27).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
after_implement task-sync: 35/94 tasks complete, status stays implementing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SteveTeece and others added 29 commits August 30, 2026 15:31
… data seeder

The debug data seeder read Settings.OrganizationName / AnnualFee /
AttendanceFee from whatever the setup wizard captured. A coordinator who
left the fees at zero got a hollow sample dataset: SeedAnnualFeesAsync
was a no-op and the door-cash -> petty-cash -> bank sweep in
SeedRehearsalsAsync moved nothing.

SeedAsync now stamps a generated organisation name ("Clarence Valley
Community Choir") and fee schedule -- annual $120, per-rehearsal $2,
February membership renewal, October committee renewal, six-seat general
committee -- over the wizard inputs before it uses them, mirroring how
sample data already supplies its own accounts, opening balances and
committee/AGM history (spec 022). Currency, language and sales-tax
treatment stay as configured. The ISettingsService.SaveAsync audit entry
is suppressed by the enclosing AuditTrailSuppressionScope like every
other seeded write.

Updates the DebugDataSeeder class summary and the app-host living spec's
"Optional sample-data seeding" section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The script deleted nothing: it targeted %AppData% (Roaming) with
forward-slash separators, which del misparses as switches. The MAUI
app writes stagefright.db to FileSystem.AppDataDirectory, which on the
unpackaged Windows head resolves to
%LOCALAPPDATA%\StageFright Community\com.stagefright.community\Data.

Rewrite the script to:
- use %LOCALAPPDATA% with backslash separators and quoted paths
- remove the SQLite WAL sidecars (stagefright.db-wal / -shm) too
- also clear a repo-root design_time.db left by "dotnet ef" runs
- guard every delete with "if exist" and report whether anything went

Also note the concrete Windows path and the script in docs/SETUP.md's
"Reset the Database" section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Settings ▸ General and Setup Wizard display-language pickers listed
only "English (Australia)" even though the app now ships en-US and fr-FR
satellite resource sets (added on top of spec 027).

Root cause: MauiProgram registered the catalog as
`AddSingleton<ISupportedLanguagesCatalog, SupportedLanguagesCatalog>()`.
Microsoft.Extensions.DependencyInjection selects the greediest resolvable
constructor, so it picked `SupportedLanguagesCatalog(IEnumerable<string>
resourceAssemblyNames)` — the test-only seam — and resolved the
unregistered `IEnumerable<string>` as an *empty* sequence (not null).
The constructor's `?? DefaultResourceAssemblyNames` guard only covered
null, so `_resourceAssemblyNames` became `[]`, `ContainsOurSatellite`
probed for nothing, no `<culture>/StageFright.*.resources.dll` folder
ever matched, and only the hard-coded `en-AU` baseline survived. It was
masked until en-US/fr-FR shipped because en-AU was previously the only
set anyway; every test used `new SupportedLanguagesCatalog()` directly or
a fake catalog, so none exercised the DI path.

- MauiProgram: register with an explicit factory
  (`_ => new SupportedLanguagesCatalog()`) so the documented parameterless
  constructor is used.
- SupportedLanguagesCatalog: the `(IEnumerable<string>)` constructor now
  treats an empty sequence like null and falls back to probing
  Core/UI/Reports — defence in depth against any DI/empty-list caller.
- Tests: SupportedLanguagesCatalogTests gains an empty-list fallback case;
  StartupSequenceTests gains a DI-container regression that resolves the
  catalog the "natural" `AddSingleton<TService,TImpl>()` way and asserts
  en-US and fr-FR are discovered (both fail without the fix, reproducing
  the "en-AU only" list).
- Refreshed three now-stale "en-AU is the only shipped set" comments in
  the picker area (GeneralSettingsTab, LanguagePickerRenderTests,
  V21_LocalizationStartupTests).

Verified in the running MAUI app via CDP: the Setup Wizard language
select now offers en-AU / en-US / fr-FR. Full rebuild 0W/0E; 2059 tests
pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… seeding (#361)

New spec-kit feature on branch 029-first-run-language-seed. Addresses issue
#361 (a saved language change is invisible until restart, with no prompt) and
folds in a requested extension: move the Debug-only sample-data choice onto the
same pre-wizard screen and strip both the language and sample-data steps out of
the setup wizard.

Scope captured in spec.md:
- P1: a dedicated first-run language screen before the setup wizard; the choice
  is persisted outside the database and applied by one automatic restart.
- P2: Settings language change shows a post-save "restart now / later" dialog
  instead of the transient inline notice.
- P3 (Debug only): a "load sample data" option on the first-run screen that
  seeds before restarting and lands on the dashboard, wizard skipped.
- Setup wizard loses its language selector, its sample-data option, and the
  tab-bypass behaviour that only existed to support in-wizard seeding.
- Startup language resolution gains a stored-preference tier between
  Settings.LanguageCode and the OS language.
- New app self-restart capability (Windows; graceful no-op elsewhere).

Supersedes the first-run-language parts of spec 027 and the in-wizard
sample-data parts of specs 017/022. No DB schema change; no GL/money change.

Requirements-quality checklist passes; two multi-interpretation decisions
(auto-restart into the wizard; replace the inline notice with a dialog) were
settled with the requester and recorded in the checklist notes rather than
left as clarification markers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rompts

Revise spec 029 so a display-language change takes effect immediately in
the running session on every path (first-run screen and Settings), with no
application restart and no "restart required" prompt, dialog or notice
anywhere. This reverses spec 027's no-in-session-switching constraint.

- First-run screen: confirm applies the language in place, then continues
  to the wizard (Release) or seeds sample data in the pre-wizard step and
  goes straight to the dashboard (Debug) - no restart on either path.
- Settings: a saved language change re-renders the UI at once; the inline
  "restart required" notice and the planned post-save restart dialog are
  both gone.
- Removed the standalone restart-capability requirements and all
  non-Windows self-restart degradation; added an in-session-switching
  requirement group. Requirements renumbered FR-001-FR-023, no gaps.
- Updated success criteria, key entities, assumptions, edge cases,
  out-of-scope, and the spec-027 supersedes note; refreshed the
  requirements checklist notes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 0/1 design artifacts for spec 029: research decisions (no-DB
preference store, in-session CultureProvider.Switch, startup routing/
ladder changes, wizard cleanup, sample-data reuse, Settings restart-
notice removal), data model, UI/service contract, and quickstart
validation scenarios.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017sTJ4fm3PqXPQS4vLXzNHe
37 tasks across Foundational + 3 user-story phases + Polish, generated
from plan.md/research.md/data-model.md/contracts. Also records the
tasks-step completion in .spec-context.json (status: ready-to-implement).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EyGsgcc7qvJL8vZLUZRttT
…+ US1 tests

Phase 2 (Foundational) + partial Phase 3 (US1) of spec 029:

- ILanguagePreferenceStore (Core contract) + MauiLanguagePreferenceStore
  (Preferences-backed impl), registered in MauiProgram
- CultureProvider.Switch(CultureInfo) — in-session live culture change,
  no restart, re-renders the whole ShellLayout-wrapped tree
- LanguageProvider's FR-006 startup ladder gains step 2 (recorded
  preference), renumbering OS-language/en-AU fallback to 3/4
- New FirstRunLanguageScreen (@page "/language-select"): lists shipped
  languages, pre-selects the resolved default, confirm records the
  preference + switches culture + navigates to /setup
- App.razor.cs routes to /language-select vs /setup based on whether a
  preference is already recorded (only while setup is incomplete)
- Tests: CultureProviderTests, FirstRunLanguageScreenTests,
  AppRoutingTests (new); LanguageProviderTests extended; SetupWizardTests,
  ReviewTabTests, LanguagePickerRenderTests updated to drop the
  sample-data/language-tab assertions being retired; SetupWizardNoSeederTests
  and SampleDataTabTests deleted outright (their premise stops existing)

Full solution build is green. SetupWizard/LanguageSelectionTab/SampleDataTab/
ReviewTab themselves are not yet edited (tasks.md T018-T023) — several of the
above tests (e.g. Tabs_AreNeverDisabled, SeedDataCheckbox_IsAbsent) will run
red until that follow-up lands, since the wizard still carries the language
step, sample-data checkbox and tab-bypass mechanism this story retires.

Tasks T001-T017 of specs/029-first-run-language-seed/tasks.md complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb
…ption

Finishes Phase 3 (US1) of spec 029, tasks T018-T023:

- Delete LanguageSelectionTab and SampleDataTab outright (spec 029 FR-016/
  FR-017: no language selector, no sample-data option, no variation in the
  wizard's step list — both retired to the new /language-select screen)
- SetupWizard.razor: drop the two tabs' markup and every Disabled="@_seedWithTestData"
  attribute — tabs are now always enabled; drop the now-dead seeding overlay
  (the wizard never seeds any more, moved to FirstRunLanguageScreen)
- SetupWizard.razor.cs: remove _seedWithTestData/_debugSeeder/IsTabBypassed/
  HandleSeedWithTestDataChanged and the IDebugDataSeeder resolution; Next
  advances one tab at a time (no bypass skip); the opening-balance Finish
  guard is unconditional; LanguageCode now comes from the cascaded
  CultureProvider.CurrentCulture, not a wizard field
- Drop the now-unwritten LanguageCode field from SetupFormModel
- ReviewTab: drop DebugSeederAvailable/SeedWithTestData params and the
  "Load sample data" summary row; drop the orphaned
  Setup_Review_LoadSampleDataTerm resx key (all 4 culture files)

Test-infrastructure fix (found via full-suite verification, not part of
tasks.md but required for a green build):
- CultureProviderTests.cs had un-awaited cut.InvokeAsync(...) calls, so
  CultureProvider.Switch's static CultureInfo mutation could still be
  in-flight when the test's restore ran — this alone caused ~116 failures
  scattered across totally unrelated tests (English-text assertions
  silently seeing French). Fixed by awaiting every InvokeAsync call.
- FirstRunLanguageScreenTests.cs's three confirm-button tests trigger the
  same Switch call via a synchronous .Click() but had no culture
  restoration at all — fixed with the same shared CultureRestorer helper
  (extracted to tests/StageFright.UI.Tests/CultureRestorer.cs).
- Even with correct per-test restoration, Switch's mutation of the
  process-wide CultureInfo.Default* statics is observable by any other
  test collection running concurrently — reproduced three times across
  separate full runs, each time corrupting a different unrelated test.
  Added AssemblyCollectionBehavior.cs (DisableTestParallelization) to
  StageFright.UI.Tests: the standard xUnit fix when many tests share
  global mutable state that virtually the whole suite reads (unlike the
  narrower per-class MoneyFormatterStateCollection precedent in
  StageFright.Integration.Tests, tagging every consumer here would mean
  tagging nearly the entire ~600-test project). Verified: 629/629 pass
  reliably across repeated full runs with this in place (was previously
  629/629, 628/629, or 625/629 depending on scheduling luck).

Full solution: dotnet build 0 warnings/0 errors; dotnet test 2062/2062
passed across all six projects (Core.Tests 751, Reports.Tests 236,
Localization.Tests 28, Data.Tests 158, UI.Tests 629, Integration.Tests 260).

User Story 1 (P1/MVP) is now independently complete and testable per
tasks.md's Phase 3 checkpoint: a clean install shows /language-select
before the wizard; confirming a language applies it immediately with no
restart; the wizard never shows a language selector or sample-data option
and its step list never varies; a recorded preference is never re-prompted.

Tasks T018-T023 of specs/029-first-run-language-seed/tasks.md complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb
The previous write-context.py --step-summary call omitted --step and
defaulted to "specify"; moved the summary to "implement" where it belongs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb
…e dropped

Completes Phase 4 (US2) of spec 029, tasks T024-T028:

- GeneralSettingsTab.razor.cs: drop the LanguageChanged property and
  _initialLanguageCode field entirely — the "was this changed" check is now
  a one-off local comparison against _settings.LanguageCode captured right
  before HandleSaveAsync overwrites it, so no dedicated field is needed.
  After a successful SettingsService.SaveAsync where the language changed,
  call ILanguagePreferenceStore.Set(_selectedLanguageCode) then
  CultureProvider.Switch(...) — the exact same record-then-switch sequence
  FirstRunLanguageScreen uses, via a newly-cascaded CultureProvider
- GeneralSettingsTab.razor: remove the "restart the app" alert block —
  there's nothing to wait for now, the change is visible the moment it's
  saved (FR-010/FR-020/SC-007)
- Drop the now-orphaned Settings_General_LanguageRestartNotice resx key
  (all 4 culture files)
- Tests: GeneralSettingsTabTests gains cases for changed-language save
  (records preference + switches culture) and unchanged-language save
  (does neither), plus an explicit "no restart notice ever, before/after
  change/save" assertion; LanguagePickerRenderTests drops the now-false
  "shows restart notice" case and broadens the "no notice" case to also
  check after a change

Full solution: dotnet build 0 warnings/0 errors; dotnet test 2064/2064
passed across all six projects (631 in UI.Tests, +2 net from these tests).

User Story 2 (P2) is now independently complete per tasks.md's Phase 4
checkpoint: changing the language in Settings and saving re-renders the
whole app in the new language in the same interaction, no restart notice
ever shown, and the choice survives a relaunch (via the already-existing
Settings.LanguageCode persistence). User Stories 1 and 2 both work
independently.

Tasks T024-T028 of specs/029-first-run-language-seed/tasks.md complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb
Phase 5 (T029-T032): FirstRunLanguageScreen now optionally resolves
IDebugDataSeeder (Debug builds only) and offers a "Load sample data"
RadzenSwitch. Confirming with it ticked builds a placeholder
SetupRequest, calls SetupService.InitializeAsync then
IDebugDataSeeder.SeedAsync under a seeding-progress overlay (same
try/finally shape the deleted SetupWizard used), and lands on
/dashboard with the wizard never shown. A seeding failure shows
Setup_FirstRun_SeedingError and stays on the screen.

Added Setup_FirstRun_SeedingError and
Setup_FirstRun_SampleOrganizationPlaceholder resx keys (neutral +
en-US/fr-FR/qps-ploc) — the placeholder org name is routed through a
key rather than a literal because Us2LocalizationGuardTests bans bare
string literals on this scanned UI surface, even for a value
DebugDataSeeder immediately overwrites.

Also fixes a genuine bUnit test-timing bug found while verifying this
task: a synchronous Click() on the confirm button doesn't reliably
await the real Task.Run seeding hop, so the two new tests use
ClickAsync(new MouseEventArgs()) instead, matching the codebase's
existing convention for async-submit buttons elsewhere.

Full solution build: 0 warnings/0 errors. Full test suite: 2067/2067
passed across all six projects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017iVwWMUFaY9D9L83qmVWbb
…erification finds in-session-switch defect

T033: CLAUDE.md Navigation + Localization sections updated for the /language-select
screen and the live CultureProvider.Switch (no restart notice; no next-launch delay).
T034: docs/localization/adding-a-language.md — language-picker references moved from
"Setup Wizard language step" to /language-select (one-minute version, SetupResource
table row, step 5, qps-ploc note); section 9 retitled and rewritten with the 4-tier
startup ladder (adds the ILanguagePreferenceStore no-DB preference tier) and the live
in-session switch for both entry points.
T035: dotnet build -c Debug 0 warnings/0 errors; full test suite 2067 pass (the sole
failure, EventFormTests.DoesNotRender_FeeOrPaidFields, is the CLAUDE.md-documented
GUID-substring flake — green in isolation); quickstart scenarios 1–5 filters all green.

T036: live CDP-driven verification on a clean install. PASS — /language-select renders
before /setup; lists en-AU/en-US/fr-FR by endonym; en-AU pre-selected; choice persists
to preferences.dat outside the DB; no process restart on confirm (PID stable); a
relaunch with the preference recorded resolves to fr-FR (log) and opens /setup fully in
French, skipping /language-select. FAIL — confirming fr-FR on /language-select does NOT
re-render the running session: /setup stayed entirely English. CultureProvider.Switch
has no visible effect in the real MAUI WebView2 host; the new language only appears
after a full app restart (the issue #361 symptom, on the first-run path). Recorded as a
blocking concern in .spec-context.json; suspected AsyncLocal CurrentUICulture pinned at
startup. Needs its own debug+fix+test pass (US1 T004/T016, US2 T027).

T037 not run (per user) — exercises the same CultureProvider.Switch mechanism (FR-009),
blocked on the same defect.

Spec left at status=implementing — NOT marked complete.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
T037 (Phase 6, deferred-until-implementation live verification) is now
confirmed by the user: in the real MAUI app, changing the Display language
in Settings → General and saving re-renders the running session in the new
language immediately, with no process restart and no "restart required"
notice at any point (before the change, after the selection changes, or
after the save). Verified in both light and dark themes.

This is the Settings entry point (FR-009). Contrary to the T036 note's
presumption that it would be "broken identically", the in-session
CultureProvider.Switch re-render IS effective here in the real WebView2
host — Settings renders inside the live cascaded CultureProvider, so
StateHasChanged propagates to its descendants.

The T036 blocking concern is a different code path (first-run
/language-select confirm → navigate to /setup, which does not re-render
live in the real app) and is unaffected — it remains OPEN and still needs
its own debug+fix+test pass. Spec left at status=implementing, NOT marked
complete.

- tasks.md: T037 checked, with the manual-verification note inline
- .spec-context.json: T037 history + task_summary + verified entry;
  currentTask → T037; last_action refreshed
- .spec-context.events.jsonl: T037 completion event appended

No code changed — docs/companion-state only, so no build/test run
(matches the prior companion-state-only commit 6416ba0).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ture globals only (T036)

The T036 live check found the spec-029 in-session switch had no effect in the
real WebView2 host: confirming a language on /language-select persisted it and
navigated on, but /setup rendered in the OLD language; only a full process
restart applied it (the issue #361 symptom).

Root cause (confirmed by a deterministic ExecutionContext.Run repro):
CultureInfo.CurrentCulture / CurrentUICulture are AsyncLocal-backed.
MauiProgram.RunStartupSequence pinned them on the startup thread; that
per-execution-context value *shadows* the DefaultThreadCurrent* globals on the
Blazor renderer's own ExecutionContext. CultureProvider.Switch updated the
globals (and set CurrentUICulture on the unwinding click-handler context), but
every queued render kept resolving the stale pinned value. A restart worked
only because startup then re-pinned the new culture. bUnit / integration tests
passed because they render synchronously on the same context that calls Switch.

Fix — set ONLY the process-wide statics, never the per-context override:
- MauiProgram.RunStartupSequence: assign CultureInfo.DefaultThreadCurrentCulture
  / DefaultThreadCurrentUICulture; drop the CurrentCulture / CurrentUICulture
  assignments.
- CultureProvider.Switch: same — assign the two DefaultThreadCurrent* globals,
  update the cascaded CurrentCulture property, StateHasChanged(). No per-context
  pin, so a second switch can't re-introduce the shadow either.

With no override anywhere, CultureInfo.CurrentUICulture reads straight through
to DefaultThreadCurrentUICulture on every render, so the switch is visible on
the very next render regardless of which ExecutionContext runs it — the
StateHasChanged() cascade re-render is unchanged.

Tests: CultureProviderTests reworked — Switch_SetsTheProcessWideCultureGlobals
asserts the real contract; new Switch_DoesNotPinAPerContextOverride_SoLater
GlobalChangesStillTakeEffect is the regression guard (a later DefaultThread
CurrentUICulture change is still observed — proof Switch pinned no override).
Build 0 warnings / 0 errors; full suite 2068/2068 (UI.Tests 635, +1).

Docs: CLAUDE.md Localization section; docs/localization/adding-a-language.md
sec 9; spec 029 contract, data-model.md, and a dated correction to research.md
Decision 2 (its "setting CurrentCulture on the calling thread reaches the next
render" rationale was the flawed premise).

Companion state: T036 concern marked resolved; verified entry added. Live
in-app re-verification of the fix (first-run path, both themes, + a T037
re-confirm) is tracked as new task T038 — bUnit renders synchronously so it
cannot reproduce the EC-boundary failure. Spec stays status=implementing until
that live check passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Localizer.FormatNamedPlaceholders: bind each distinct {token} to an arg
  once and substitute every occurrence by name, so a repeated token renders
  its value instead of leaking the literal "{token}" once args run out.
- Enum key format (Enum_<Type>_<Member>) now lives in one place —
  EnumLocalizationExtensions.EnumResourceKey — used by both LocalizeEnum and
  Localizer.Enum instead of being rebuilt in each.
- SetupService.Validate now range-checks FinancialYearStartMonth (1..12),
  matching the existing day check; new Validation_Setup_FinancialYearStartMonthRange
  key added to all four ValidationResource resx files. Contract doc updated.
- SupportedLanguagesCatalog: materialise the satellite-folder list eagerly so
  an IO/permission failure during enumeration is caught, not thrown mid-foreach
  out of BuildCatalog where Lazy<T> would cache it and break the catalog forever.
- FirstRunLanguageScreen sample-data path: guard InitializeAsync with
  IsSetupCompleteAsync so re-pressing Confirm after a failed seed retries
  seeding instead of dead-ending on "setup already completed". Spec 029 docs
  updated.
- GeneralSettingsTab code-behind: resolve the save-success message through the
  already-injected ILocalizer, consistent with its other lookups.
- CultureProvider: drop the redundant OnInitialized (field initialiser already
  sets CurrentCulture to the same value).
- MoneyFormatter: document the process-wide-static test-isolation contract.

Review items #8 (RadzenSwitch <label for>) reviewed and dismissed — Radzen
renders a real labelable <input id="{Name}"> so the association is valid.

Tests: +7 (LocalizerTests placeholder cases, FY-start-month range, seed-retry
after failure). Full solution build clean (0 warnings); 2076 tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
029: first-run language selection & sample-data seeding (also carries specs 027 & 028)
The `build-mac-app` job passed `-p:RuntimeIdentifier="maccatalyst-x64;maccatalyst-arm64"`.
Shell quoting stops the shell from splitting on `;`, but MSBuild's own `-p:` switch
parser still treats `;` as a property-list delimiter, so it read `maccatalyst-arm64`
as a second, malformed property and failed:

    MSBUILD : error MSB1006: Property is not valid.
    Switch: maccatalyst-arm64

Join the two RIDs with the MSBuild literal-semicolon escape `%3B` instead. MSBuild
un-escapes it to `;` during property parsing, yielding the single property value
`maccatalyst-x64;maccatalyst-arm64` that the xamarin-macios targets read as a
universal (lipo'd x64 + arm64) build.

Also updates the CLAUDE.md description of the workflow to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fix(ci): escape RID separator in macOS debug pre-release publish (#366)
The macOS job in debug-pre-release.yml failed with:

  NETSDK1083: The specified RuntimeIdentifier
  'maccatalyst-x64;maccatalyst-arm64' is not recognized

It was passing the singular -p:RuntimeIdentifier with a semicolon list.
The .NET SDK validates a lone RuntimeIdentifier as a single RID and
rejects a list. MAUI's Mac Catalyst targets only rewrite a ;-list in the
singular property into the plural RuntimeIdentifiers when it is set inside
the .csproj (as MAUI does for Release builds) - not when it arrives as a
command-line global property. A Debug CLI universal build must therefore
pass the plural -p:RuntimeIdentifiers itself.

The %3B escaping is unchanged and still required (issue #366).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ntifiers

ci: use plural RuntimeIdentifiers for the Mac universal debug build
Passing -p:RuntimeIdentifiers=maccatalyst-x64%3Bmaccatalyst-arm64 on the
command line made it a global property that propagated verbatim to every
ProjectReference. The plain net10.0 class libraries (Core, Data, UI,
Plugins.Contracts, Reports) then failed with NETSDK1083 on the joined
';'-list.

Instead, add a conditional PropertyGroup to StageFright.App.csproj that
sets the plural <RuntimeIdentifiers> only when -p:PublishMacUniversal=true
is passed for a maccatalyst TFM, and have the workflow pass that flag. As
a project property it is evaluated locally and MAUI's Mac Catalyst targets
fan out per-RID inner builds that hand a single concrete RID to the
referenced libraries. This mirrors dotnet/maui's own
Controls.TestCases.HostApp.csproj CI pattern. The group is inert for every
normal local/CI build, so single-arch Debug builds and `dotnet run` are
unaffected.

Verified: dotnet build src/StageFright.App (Debug, net10.0-windows) still
succeeds with 0 warnings/errors - the new group stays dormant off-target.
The universal maccatalyst build itself can only be exercised by re-running
the workflow on a macOS runner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
MoneyFormatter.FormatCore delegated the negative-amount representation
entirely to CultureInfo.CurrentCulture, so a stored AUD figure rendered
"-$42.10" on an en-AU/en-US host but "($42.10)" under the invariant
culture that CI hosts default to. That broke the spec 028 FR-006 / SC-004
zero-drift regression (AudZeroDriftTests) on CI while passing locally.

FormatCore now sets NumberFormatInfo.CurrencyNegativePattern to the
leading/trailing-minus form matching the active culture's symbol
placement (never accounting-style parentheses). Positive formatting,
grouping, separators and symbol placement are unchanged; fr-FR still
trails the symbol ("-42,10 $"). AUD output is now byte-identical to the
pre-028 string on every host.

Adds MoneyFormatterTests coverage pinning en-AU / en-US / invariant /
fr-FR. Updates CLAUDE.md and specs/028 plan.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rids

ci: set Mac universal RIDs in the app csproj, not on the publish CLI
* spec(027): add localization support spec, plan, and tasks

Feature 027 introduces per-language resource files (Australian English
baseline) so every screen, report, menu, tile, exception message and
user-facing enum value reads its text via IStringLocalizer, plus a
persisted, OS-aware Settings.LanguageCode selection.

Adds the full spec-kit artifact set:
- spec.md - 3 prioritized user stories, FR-001..FR-024, SC-001..SC-010
- plan.md, research.md - IStringLocalizer + ~12 area .resx markers,
  missing-key logging decorator, MoneyFormatter (fixed AUD), startup
  culture ladder (explicit -> OS language -> en-AU)
- data-model.md - Settings.LanguageCode column + migration outline
- contracts/ - localization interfaces + resource-key catalog/guards
- quickstart.md, checklists/requirements.md
- tasks.md - 64 tasks (T001-T064) in 6 wave-grouped phases

Bumps .specify/feature.json to specs/027-localization-support.



* feat(027): localization Setup + Foundational infrastructure (T001-T014)

Phase 1 (Setup):
- Add Microsoft.Extensions.Localization 10.0.10 to Directory.Packages.props
- Scaffold StageFright.Localization.Tests guard-test project (refs Core/UI/Reports)
- Add version-less PackageReference + NeutralLanguage=en-AU to Core/Reports/UI csproj

Phase 2 (Foundational):
- 12 area resource marker classes + empty neutral .resx: NavigationResource,
  ValidationResource, EnumsResource (Core); ReportsResource (Reports);
  Shared/Dashboard/Members/Rehearsals/Events/Finance/Settings/SetupResource (UI)
- ILocalizer facade + Localizer impl (Get/Get-with-args/Plural/Enum), with
  named-placeholder substitution by order of first appearance (FR-010)
- MissingKeyLoggingLocalizerFactory: IStringLocalizerFactory decorator that logs
  a Warning and falls back to neutral on a missing key (FR-008/FR-009)
- EnumLocalizationExtensions.LocalizeEnum: Enum_<Type>_<Member> lookup via a
  factory holder set once at startup (FR-024)
- MoneyFormatter.Format/FormatWithCode: fixed "$"/"AUD" symbol, culture-driven
  separators/grouping (FR-015)
- Guard-test scanning infrastructure (LocalizerKeyUsageScanner, ResxKeyScanner,
  PlaceholderTokenScanner, UserFacingEnumScanner) — no assertions yet
- LocalizedTestContext bUnit base wiring AddLocalization() + LocalizeEnum
- DI wiring in MauiProgram: AddLocalization(), decorated IStringLocalizerFactory,
  AddScoped<ILocalizer, Localizer>(), EnumLocalizationExtensions.UseFactory at startup

Full solution build: 0 warnings, 0 errors. Full test suite: 1693 existing tests
pass unchanged. User stories 1-3 and Polish (T015-T064) remain undone.



* chore(027): record after_implement companion hook state (14/64 tasks)



* feat(027): localize navigation shell + Members module (US1, T015-T028)

Phase 3 / User Story 1 — the extraction pattern proven end-to-end on the
navigation shell plus the complete Members module. Same en-AU wording, now
sourced from IStringLocalizer<T> resources.

Resources (T017-T021):
- NavigationResource / MembersResource / ValidationResource / EnumsResource /
  SharedResource .resx populated with verbatim en-AU entries (labels, headings,
  buttons, placeholders, aria-label/alt/title, plural pair, enum display text).

Call sites (T022-T028):
- ShellLayout, ThemeProvider (no-op — no user text), MemberMenuItemProvider,
  MemberValidationService, MemberList/MemberDetail/MemberForm (.razor+.razor.cs),
  MembersTile, MembersDashboardTileProvider read text via IStringLocalizer.
- MemberStatus + Theme render through LocalizeEnum() (FR-024); fee amounts via
  MoneyFormatter.Format (FR-015); parameterised strings via ILocalizer helper
  methods in code-behind.
- Completed the T011 gap: @using StageFright.Core.Localization in _Imports.razor.
- DI unchanged — all three localizer-taking registrations resolve from
  AddLocalization() + the decorating factory.

Tests (T015-T016):
- New StageFright.Localization.Tests US1 guard suite: baseline completeness
  (+plural pairing), MemberStatus/Theme enum-key coverage, residual-literal scan
  over the US1 slice, no-raw-enum-render, no-"C"-currency-format, missing-key
  warning + neutral fallback.
- New FR-018 bUnit tests assert US1 text resolves via resource keys / LocalizeEnum.
- 5 existing bUnit classes moved to LocalizedTestContext; it now also registers
  ILocalizer. StubStringLocalizer / RealStringLocalizer test helpers added for
  the constructor-injection sites.

Full build clean; 1706 tests pass (Localization 6, Reports 178, Core 597,
UI 587, Data 152, Integration 186). US2/US3/Polish not started.



* feat(027): localize Rehearsals module (US2, T032)

Move every user-facing literal in Pages/Rehearsals/** into
RehearsalsResource.resx (en-AU verbatim) read through IStringLocalizer
at render time: RehearsalList, RehearsalForm and AttendanceGrid .razor
+ .razor.cs, including aria-label/placeholder text (FR-001). The
attendance fee now formats through MoneyFormatter, not ToString("C")
(FR-015). Parameterised strings (search term, dates, member names,
attendance rate) resolve via ILocalizer.Get in code-behind helpers.

RehearsalFormModel.Date drops its dead DataAnnotations ErrorMessage
literal (RequiredAttribute never fires on a non-nullable DateTime).
Three catch blocks stop surfacing raw ex.Message, matching the US1
MemberForm precedent.

Tests: new Rehearsals-scoped guard suite (baseline completeness,
residual-literal incl. accessibility attrs, no-"C"-currency) as an
interim stand-in for the still-pending repo-wide T029/T030; new
FR-018 render tests; RehearsalListTests + AttendanceGridTests flipped
to LocalizedTestContext (resx values byte-identical to prior copy).

Full rebuild clean (0 warnings). 1714 tests pass (+8 from the US1
baseline). 29/64 tasks; US2 continues with T029/T030 + T031/T033-T045.



* chore(027): record after_implement companion hook state (29/64 tasks)



* feat(027): localize Dashboard page + all dashboard tiles (US2, T031)

Dashboard page chrome, the six remaining dashboard tile bodies (Events,
Rehearsals, AttendanceTrend, Finance, CashFlow, OutstandingBalances) and
their tile providers now read every user-facing string from
DashboardResource / EventsResource / FinanceResource / RehearsalsResource
/ SharedResource. Chart dataset legend labels localized; tile fee/balance
amounts moved from .ToString("C") to MoneyFormatter (FR-015). Tile
providers ctor-inject IStringLocalizer<T> (matching MembersDashboardTile
provider). bUnit tile/dashboard test classes flipped to
LocalizedTestContext; provider-test + V8 integration ctors updated.



* feat(027): localize remaining menu providers + author enum resource keys (US2, T038 + T042 groundwork)

All six navigation menu providers (Dashboard, Rehearsals, Events, Finance,
Settings, Reports) ctor-inject IStringLocalizer<NavigationResource> and
source Title/ShortLabel/sub-item labels from NavigationResource.resx
(matching MemberMenuItemProvider). EnumsResource.resx gains
Enum_<Type>_<Member> keys for FeeType, PaymentMethod, PaymentType,
AccountType, TaxCode, ReconciliationStatus and JournalEntryType (call-site
LocalizeEnum swaps land with each module + T042). Core.Tests gains a
RealStringLocalizer fixture; the two affected menu-provider test ctors updated.



* feat(027): localize shared components (US2, T037)

AddAccountForm, BorderedListBox, OpeningBalanceEntryForm,
ReactivationForgivenessDialog, ReportViewer and TileRenderer now read all
user-facing text (labels, headings, buttons, placeholders, aria-labels,
status/error messages, plural button labels) from SharedResource.resx.
AccountType options and account-type column render via LocalizeEnum;
forgiveness fee amounts and report money via MoneyFormatter; parameterised
strings resolved in code-behind helpers. SubmitButtonText / EmptyText
params made nullable with a localized fallback. 16 bUnit test classes that
render these components flipped to LocalizedTestContext. Full suite green
(1714).



* feat(027): localize Events / AGM module (US2, T033)

All 10 Events/AGM screens (EventList, EventForm, EventDetail,
ParticipationGrid, AgmList, AgmDetail, ScheduleAgm, RecordAgm,
RecordSpecialElection, AgmAttendanceGrid) plus AgmAttendanceReportPrinter
read every user-facing string, aria-label, badge, status text, validation
and print message from EventsResource.resx / SharedResource. Parameterised
strings via ILocalizer code-behind helpers; EventFormModel data-annotation
messages dropped to bare [Required] (dead on non-nullable value types,
matching US1). 8 Events bUnit test classes flipped to LocalizedTestContext.
Full suite green.



* feat(027): localize Settings page + tabs (US2, T035)

SettingsPage tab titles/error-boundary text and the General, Sales Tax,
Committee, Event Types and Backup & Restore tabs read all user-facing
strings, labels, help text, badges, aria-labels, status/error/success
messages and validation from SettingsResource.resx / SharedResource. Theme
label via LocalizeEnum; TaxCode options via LocalizeEnum (value stays the
invariant token); audit-retention option pluralised via ILocalizer.Plural;
fee input adornments fixed to "$" per FR-015 (was culture CurrencySymbol).
Language picker row deliberately left for US3/T056. 5 Settings bUnit test
classes flipped to LocalizedTestContext. Full suite green.



* feat(027): localize Setup Wizard (US2, T036)

SetupWizard shell + all 9 tab components (GeneralAppearance, MembershipFees,
ThemeSelection, SalesTax, SampleData, ChartOfAccounts, OpeningBalances,
Committee, Review) read every user-facing string, label, help text,
placeholder, aria-label, badge and validation/error message from
SetupResource.resx. Review money via MoneyFormatter; Theme + TaxCode via
LocalizeEnum (wizard's Light/Dark <select> keeps invariant option values
per spec 017 FR-022); audit-retention & review lines pluralised via
ILocalizer.Plural; fee adornments fixed to "$". Language step deliberately
left for US3/T057. 5 Setup tab bUnit test classes flipped to
LocalizedTestContext. Full suite green.



* feat(027): localize StartupError page + Reports page shell (US2, T043)

StartupError.razor (database recovery page) and ReportsPage.razor read
their headings, body text, buttons, prompts and error messages from
SharedResource.resx. MainPage.xaml / App.xaml carry no user-facing text.
Remaining Pages/** (Finance) is T034.



* feat(027): localize Finance module (US2, T034)

Move every user-facing literal in the 13 Finance pages + code-behinds
into FinanceResource.resx (en-AU baseline). Fee/payment/account/tax/
reconciliation-status enums render via LocalizeEnum; every displayed
amount goes through MoneyFormatter (FR-015) instead of ToString("C") /
FormatString="{0:C}". aria-labels, placeholders and the embedded-link
"no accounts configured" warnings are localized too (FR-001).

Nine Finance bUnit test classes flipped to LocalizedTestContext so
IStringLocalizer<FinanceResource> resolves; neutral values are
byte-identical to the prior hardcoded copy so assertions stay green.

Full solution build clean; full suite 1714 passing.



* feat(027): localize AgeCalculationService DOB/age messages (US2, T041 groundwork)

The US1-deferred piece of T041: AgeCalculationService.ValidateDateOfBirth
now sources its three user-facing messages from ValidationResource via the
ILocalizer facade (named {Age}/{MaxYears}/{MinimumAge} placeholders), so the
service is DI-constructed with ILocalizer.

Call-site fan-out handled: MemberList/MemberDetail switch from `new()` to an
injected AgeCalculationService (registered in LocalizedTestContext for bUnit);
Core/Integration/Reports test projects each gain a RealLocalizer fixture and
pass it to the ~12 `new AgeCalculationService(...)` sites.

The remaining T041 work — service-thrown ValidationException/DomainValidation-
Exception message text across ~18 Core services — is not in this commit.

Full solution build clean; full suite 1714 passing.



* chore(027): record companion context after T034 + T041 groundwork

* feat(027): localize the 11 report providers (US2, T039 + T030)

Route every report provider's user-facing text through the ILocalizer
facade against a new populated ReportsResource.resx (~120 en-AU entries,
transcribed verbatim per FR-004): report names, filter labels,
titles/subtitles (named-placeholder formatted), section headings, column
headers, subtotal/total + inline row labels, and the GL-imbalance
exception message. Enum-valued cells (MemberStatus, ReconciliationStatus)
resolve via ILocalizer.Enum (FR-024).

Filter option VALUES stay culture-invariant tokens (Options, unchanged —
still compared in GenerateAsync and persisted); the localised option
LABEL is carried on a new parallel ReportFilterDefinition.OptionLabels
list, rendered by ReportViewer with fallback to the token.

Report amounts keep decimal.ToString("F2") — providers never used "C",
and adding a $ symbol/grouping would break SC-002 render parity. Report
renderers (T040) and service exception text (T041) remain.

T030: ReportsResourceLabelTests (names/headers/section+total labels
resolve via ReportsResource, no raw-key/blank leak — FR-006) and
PluginTextNonEnglishCultureTests (TestPlugin English report + tile text
renders unchanged under fr-FR, run on a dedicated thread so the culture
never leaks into parallel tests — FR-020).

Build 0 warnings / 0 errors; full suite 1733 pass / 0 fail
(Reports 178->195, Integration 186->188).



* chore(027): record after_implement companion context (39/64 tasks)



* feat(027): localize report PDF renderer chrome (US2, T040) + enum/currency sweeps (T042, T044)

T040: all 6 PDF report renderers now source their page chrome from
ReportsResource.resx (~21 en-AU keys, verbatim per FR-004):
- PdfReportRenderer: "Generated: {DateTime} UTC", footer "Page"/"of"
- AttendanceRollPdfRenderer: title, "Rehearsal: {Date} at {Time}", Name/Present/Pd headers
- AgmResultsPdfRenderer: title, meeting/attendance lines, "Elected Positions",
  "No positions recorded.", bold "{Label}: " position prefix, "General Committee Member:"
- CheckboxSheetPdfBuilder: shared "Name" column + footer; gains an ILocalizer param
  and a nameColumnHeader arg threaded to BuildMemberTable
- Event/AgmAttendanceSheetPdfRenderer: titles, date lines, Participated/Attended headers
The 5 concrete renderers ctor-inject ILocalizer (resolves from the existing
AddScoped<ILocalizer, Localizer>() - no MauiProgram change). CsvReportExporter has
no user-facing literals and is untouched. 8 direct test-construction sites now pass
RealLocalizer.Instance; ReportsResourceLabelTests gains 22 chrome-key + placeholder
assertions (FR-006).

T042: verified the user-facing enum sweep is complete - EnumsResource.resx holds
every Enum_<Type>_<Member> key for all 9 user-facing enums. Fixed the one residual
raw display site (the item deferred from US1): MemberDetail Fee Payment History
FeeType column now uses fee.FeeType.LocalizeEnum().

T044: verified zero decimal.ToString("C")/{0:C}/FormatString display sites remain in
UI or Reports. The one user-facing residual - BankReconciliationService.cs:153
ReconciliationException message - is exception-message text owned by T041.

Full solution build: 0 warnings / 0 errors. Full test suite: 1755 passed / 0 failed
(Reports 217, up from 195).

US2 remaining: T041 (user-facing exception Message text across ~18 Core services -
high blast radius, own session), T045 (DI recheck after T041's ctor changes),
T029 (repo-wide guard suite - blocked until T041).



* feat(027): localize non-Finance service exception messages (US2, T041 part 1)

Route user-facing ValidationException / DataIntegrityException / ImportException
Message text through ValidationResource for the eight non-Finance Core services:
AgmService, EventService, EventTypeService, CommitteeOfficeHolderTypeService,
AttendanceService, SettingsService, SetupService, BackupService.

- 34 new en-AU keys in ValidationResource.resx (verbatim wording, FR-004);
  named-placeholder keys for {Year}/{SchemaVersion}/{SupportedMajorVersion}/{EntityType}.
- Each service takes the ILocalizer facade via a required ctor param (matches the
  AgeCalculationService T041-groundwork precedent; only path that substitutes named
  tokens). All eight are plain AddScoped<I,Impl> registrations so DI auto-resolves
  the new param -- no MauiProgram change.
- BackupService DeserializeAndValidate / ValidateVersion / ValidateCompleteness
  change from private static to instance to read _localizer.
- EntityNotFoundException built-in message and DataAccessException wrappers stay
  English (diagnostic per FR-007 -- UI never shows them verbatim).
- 20 test call-sites updated to pass RealLocalizer.Instance; new RealLocalizer
  fixture added to StageFright.Data.Tests.
- Interim guard Us2ExceptionMessageGuardTests (baseline completeness + no residual
  literal) scoped to the eight files.

Full build 0 warnings / 0 errors. Full suite 1757 pass.

Remaining for US2: T041 Finance services (incl. BankReconciliationService {Difference:C}),
T045 DI recheck, T029 repo-wide guard suite.



* feat(027): finish US2 — localize Finance exception messages + repo-wide guard (T041, T029, T045)

T041 (Finance half): route user-facing ValidationException / ReconciliationException
Message text through ValidationResource for all 9 Finance services — AccountService,
BankReconciliationService, ExpensePaymentService, PaymentService, FeeService,
GeneralJournalService, IncomeEntryService, BankDepositService, OpeningBalanceService.

- 39 new verbatim en-AU ValidationResource keys (FR-004) + reuse of
  Validation_Settings_NotConfigured; named-token keys for {Name} (Account duplicate),
  {Date} (statement date), {Difference}/{Zero} (reconciliation finalise).
- BankReconciliationService finalise message: the {workspace.Difference:C} + literal
  "$0.00" (the T044-flagged C-format residual) now routes both amounts through
  MoneyFormatter.Format — byte-identical under en-AU, fixed-AUD under any culture (FR-015).
- Each service takes the ILocalizer facade via a required ctor param (matches the
  non-Finance T041 precedent). All 9 are plain AddScoped<I,Impl> so DI auto-resolves
  the new param — no MauiProgram change (T045).
- ~30 test ctor sites pass RealLocalizer.Instance (Core.Tests, Data.Tests,
  Integration.Tests). EntityNotFoundException / GLBalanceException wrappers stay
  English (diagnostic per FR-007).
- Us2ExceptionMessageGuardTests extended to cover the Finance services.

T029: new repo-wide Us2LocalizationGuardTests (8 facts) — baseline key completeness
across the US1+US2 surface, Enum_<Type>_<Member> coverage for all 9 user-facing enums
+ no-raw-enum-render, residual-literal scan over the UI display surface, orphan-satellite
keys, plural-pair + placeholder parity, cross-culture token parity, and no-"C"-currency
repo-wide. Report-provider filter option-VALUE tokens (guarded by ReportsResourceLabelTests,
T030) and DataAnnotations ErrorMessage / ValidationResult literals (compile-time constant,
resolved in T060) are documented exclusions.

resource-key-catalog.md §3 updated to reflect the implemented guard layout and exclusions.

Full rebuild 0 warnings / 0 errors. Full suite 1765 pass (Localization 19, Reports 217,
Core 597, UI 592, Data 152, Integration 188). US2 (Phase 4) complete — 45/64 tasks.



* feat(027): US3 — persisted, OS-aware display language + pickers (T046-T059)

Adds the language-selection slice on top of the US1/US2 extraction:

- Settings.LanguageCode (nullable, presentation-only) + hand-written
  EF migration AddLanguageCodeToSettings (dotnet ef can't build the MAUI
  startup project here); SchemaVersion left at 1.1.0 (unchanged for the
  last ~6 settings migrations; asserted verbatim by backup tests).
- ILanguageProvider FR-023 startup ladder (explicit choice -> OS display
  language by exact then parent-language match -> en-AU), wired in
  MauiProgram to set the process culture before the first Blazor render;
  ISystemCultureProvider seam (SystemCultureProvider in the App layer).
- SupportedLanguagesCatalog: runtime discovery of shipped resource
  cultures by scanning satellite folders for the StageFright assemblies;
  qps-* pseudo-locales excluded; endonyms from CultureInfo.NativeName.
- Settings General tab + Setup Wizard language pickers (endonym <select>,
  active-marked, inline restart notice at the point of change - FR-021);
  choice flows through SetupRequest -> Settings at first-run Finish.
- CultureProvider cascading seam wrapping ShellLayout (parallel to
  ThemeProvider; no v1 behaviour change) - named CultureProvider to
  avoid colliding with the Core LanguageProvider service.
- qps-ploc test pseudo-locale: scripts/generate-pseudo-locale.py + 12
  regenerable <Marker>.qps-ploc.resx fixtures (bracketed/accented,
  {tokens} preserved, 3 keys omitted to exercise per-key fallback).
- Tests (+40): LanguageProvider ladder, catalog runtime discovery +
  qps-* exclusion, migration round-trip, V21 startup/SC-006 no-data-
  change, qps-ploc e2e, language-picker bUnit. LocalizedTestContext
  registers the new language services.

Full rebuild: 0 warnings / 0 errors. Full suite: 1805 pass, 0 fail
(Localization 23, Core 620, Reports 217, UI 599, Data 154, Integration 192).
Polish (T060-T064) remains.



* feat(027): Polish — repo-wide literal guard + localize App.razor + docs (T060-T064)

T060: fold the interim per-slice residual-literal scans into one repo-wide guard
(Us2LocalizationGuardTests.Should_HaveNoUserFacingLiteral_When_AppSurfaceScanned)
covering all of StageFright.UI + every user-facing exception Message literal in
StageFright.Core. The widened scan flagged App.razor (project root, outside the old
4-dir scope): localized its error-boundary + not-found chrome to 6 new Shared_App_*
keys (neutral + qps-ploc). DataAnnotations ErrorMessage args are frozen as the one
documented carve-out.

T061: docs/localization/adding-a-language.md — translator/maintainer guide (FR-022, SC-009).
T062: CLAUDE.md gains a Localization section + module-list entry + money-column data-grid
note; resource-key-catalog.md updated to past tense; spec 007 account-balance contract
gets a forward-note (its {0:C} columns moved to MoneyFormatter).
T063/T064: full rebuild 0 warn / 0 err; full suite 1805 pass (Localization 23).

Spec 027 status: completed (64/64 tasks).



* feat(027): add American English (en-US) resource set

Drop-in en-US satellite for all 12 area .resx files, translated from the
en-AU neutral baseline. The runtime SupportedLanguagesCatalog discovers it
automatically — no code change — so it now appears in Settings ▸ General
and the Setup Wizard language step.

Divergence from en-AU is spelling + vocabulary only (the two dialects are
otherwise identical across this corpus):
- organisation → organization  (Settings/Setup labels, Tax Summary subtitle)
- finalise / finalised → finalize / finalized  (Finance button, reconciliation
  validation messages, ReconciliationStatus enum label)
- cheque → check  (PaymentMethod enum label)
- cancelled → canceled  (report-generation message)
- "financial year" → "fiscal year"  (Settings + opening-balances wizard copy)

Keys, {named} placeholders and <comment> docs are byte-identical to neutral,
so the Us2 orphan-key / placeholder-parity guards pass.

Also make three previously culture-fragile bUnit assertions resolve their
expected text through the same IStringLocalizer the component uses, so they
hold under a US UI culture (GitHub CI runs windows-latest = en-US):
- ReconciliationWorkspaceTests: assert ReconciliationStatus.Finalised.LocalizeEnum()
- SetupWizardTests: assert the localized "Organisation Settings" tab title
- refresh the now-stale "v1 ships only en-AU" note in SupportedLanguagesCatalog

Full suite green under both the default culture and -culture en-US
(UI 599, Core 620, Data 154, Reports 217, Integration 192, Localization 23).



* feat(027): add French (fr-FR) resource set

Full French translation of all 12 area .resx files, dropped in beside the
en-AU neutral baseline. SupportedLanguagesCatalog discovers fr-FR at runtime —
no code change — so "français (France)" now appears in Settings ▸ General and
the Setup Wizard language step, and an fr / fr-FR operating system resolves to
it on first run. Selecting it renders the whole app in French with French
number/date formatting; the currency stays "$" / "AUD" (MoneyFormatter).

- ~1,000 <value> strings translated; keys, {named} placeholders, _One/_Other
  plural pairs and <comment> docs are byte-identical to neutral, so the Us2
  orphan-key / placeholder-parity / plural-pairing guards pass.
- AGM → "AGA" (assemblée générale annuelle); accounting terms use standard
  French (plan comptable, grand livre, bilan, compte de résultat, rapprochement
  bancaire, produits/charges, capitaux propres, écriture de journal…).
- Embedded-HTML values keep their <a href> markup; significant leading/trailing
  spaces preserved.

No test changes needed: the only test that deliberately runs under fr-FR
(PluginTextNonEnglishCultureTests) still passes, and every other test runs
under the machine culture (en-GB locally / en-US on CI) where the fr-FR
satellite is not loaded.

Full suite green under both the default culture and -culture en-US
(Core 620, Data 154, Reports 217, UI 599, Integration 192, Localization 23).



* spec(028): add international accounting-practice readiness spec, plan, and tasks

Feature 028 readies the finance module for community/amateur-theatre
groups outside Australia, judged against universal double-entry and
bookkeeping good practice (not formal IFRS/GAAP). Sourced from GitHub
issue #341 and sub-issues #342-#352.

Adds the full spec-kit artifact set:
- spec.md - 10 prioritized user stories (currency, regional number
  entry, statement consistency, basis-of-accounting disclosure, bank
  reconciliation, prior-year period locks, financial-year start,
  audit-history retention, written policies, sales-tax i18n spike),
  FR-001..FR-033, SC-001..SC-013
- plan.md, research.md - configurable org currency replacing hard-coded
  AUD, culture-safe money parsing, period-lock service, report
  integrity checks + disclosures
- data-model.md - currency/period-lock/policy fields + migration outline
- contracts/ - currency-formatting, period-lock, reports, and
  settings-and-setup contracts
- checklists/requirements.md
- tasks.md - 94 tasks (T001-T094) across 13 phases

Bumps .specify/feature.json to specs/028-international-accounting-standards.



* feat(028): foundational currency primitives + Settings schema (T001-T008)

Phase 1 baseline confirmed green (1805 tests). Phase 2 lays the shared
foundation every user-story phase builds on — no user-visible behaviour
change yet; an AUD dataset formats byte-identically.

- SupportedCurrency record + CurrencyCatalog (curated ISO 4217 seed set:
  AUD/USD/EUR/GBP/NZD/CAD 2-digit, JPY 0-digit, KWD/BHD 3-digit) with
  case-insensitive TryGet (false + Default, no throw) and Get (throws
  ValidationException on an unknown code). Mirrors SupportedLanguagesCatalog.
- MoneyFormatter gains Configure(SupportedCurrency); Format/FormatWithCode
  now use the configured symbol/code and MinorUnitDigits while grouping and
  placement still follow CultureInfo.CurrentCulture. Falls back to
  CurrencyCatalog.Default (AUD/$/2) before Configure is called.
- MauiProgram configures MoneyFormatter for Settings.CurrencyCode right
  after the display culture is applied; failure leaves it on AUD default.
- Settings entity + EF config: add CurrencyCode ("AUD"),
  FinancialYearStartDay (1), ClosedThroughDate (null); AuditRetentionYears
  default 1 -> 5 (range 1-7 unchanged).
- Migration AddInternationalAccountingSettings: adds the three columns
  (NOT NULL DEFAULT backfills the existing row), alters only the
  AuditRetentionYears column default (no UpdateData, so a configured value
  is preserved).

Build 0W/0E; all 1805 tests green.



* feat(028): US1 configurable currency — MoneyFormatter drives all money display (T009-T028)

Phase 3 / User Story 1 (spec 028). Every screen, report, PDF and CSV amount now
renders in the organisation's configured currency (Settings.CurrencyCode, ISO 4217,
default AUD) with the right symbol and minor-unit precision; grouping/placement still
follow the active culture. An AUD dataset is byte-identical to the pre-028 output.

- TaxCalculator.SplitInclusive: optional minorUnitDigits (default 2); all callers pass
  CurrencyCatalog.Get(settings.CurrencyCode).MinorUnitDigits.
- First-run setup: mandatory #setup-currency picker on GeneralAppearanceTab bound to
  SetupFormModel.CurrencyCode; SetupService validates it against CurrencyCatalog
  (Validation_Setup_CurrencyUnknown) and persists the normalised code.
- SettingsService.SaveAsync rejects a post-setup currency change
  (Validation_Settings_CurrencyImmutable) — currency is fixed after setup.
- All StageFright.Reports providers + JournalEntryPage totals route money through
  MoneyFormatter.Format instead of ToString("F2")/"N2".
- New resource keys (neutral + en-US + fr-FR): Validation_Setup_CurrencyUnknown,
  Validation_Settings_CurrencyImmutable, Setup_General_CurrencyLabel/Help.
- Tests: CurrencyCatalogTests, MoneyFormatterTests, TaxCalculator 0/2/3-digit theory,
  Setup/Settings currency cases, CurrencyPickerTests (bUnit), AudZeroDriftTests +
  V28_CurrencyConfigurationTests (integration acceptance). MoneyFormatterState
  collection serialises MoneyFormatter.Configure-sensitive integration tests.
- Updated ~30 pre-existing report/UI/integration assertions to MoneyFormatter.Format(...).
- Docs: CLAUDE.md Localization note + spec 027 superseding notes for the fixed-$/AUD prose.

Full rebuild: 0W/0E. Tests: 1868 pass (was 1805).



* feat(028): US2 locale-safe money entry — MoneyInput.Parse for numeric inputs (T029-T035)

Phase 4 / User Story 2 (spec 028). The manual journal and opening-balance forms
hand-rolled decimal.TryParse(value, NumberStyles.Number, CultureInfo.CurrentCulture)
on the value of an <input type="number"> — which the browser always serialises
invariant ("1.50"). Under fr-FR / de-DE the period was read as a thousands
separator and 1.50 posted to the ledger as 150 (FR-007…FR-009 / live data bug).

- New MoneyInput.Parse(string?): invariant decimal.TryParse with
  NumberStyles.AllowDecimalPoint | AllowLeadingSign; 0m for null/blank/unparseable.
  The single shared money-entry parse helper.
- JournalEntryPage.ParseAmount and OpeningBalanceEntryForm.SetAmount now delegate
  to MoneyInput.Parse; removed the CurrentCulture parse and the now-unused
  System.Globalization using from both.
- Tests: MoneyInputTests (unit, fr-FR/de-DE/en-AU), JournalEntryPageLocaleTests +
  OpeningBalanceEntryFormLocaleTests (bUnit — entered amount stored exactly),
  MoneyInputGuardTests (repo-wide: no UI money field parses with CurrentCulture;
  both handlers route through MoneyInput.Parse), V28_LocaleSafeMoneyEntryTests
  (integration acceptance — real journal/opening-balance services over SQLite,
  fr-FR/de-DE input stored exact to the cent and identical to en-AU).

Build: 0W/0E. Tests: 1895 pass (was 1868, +27).



* chore(028): sync companion after_implement context (Phase 4 / US2)

after_implement task-sync: 35/94 tasks complete, status stays implementing.



* chore(028): record plug-display concern for Polish T091a



* feat(028): US3 statement integrity — Balance Sheet / Trial Balance guards (T036-T040)

TrialBalanceReportProvider drops the 0.01 tolerance band: the imbalance
check is now `totalDebits != totalCredits`, so any non-zero difference
(including one cent) refuses to generate — no tolerance band (FR-011).

BalanceSheetReportProvider appends an explicit emphasized out-of-balance
line (new `Reports_BalanceSheet_OutOfBalance` key + `MoneyFormatter.Format`
of the difference) whenever total assets != total liabilities + equity, so
a corrupted ledger never yields a clean statement (FR-010).

Reworded `Reports_TrialBalance_GLImbalanceError` across neutral/en-US/fr-FR
to drop the tolerance implication and the doubled literal `$` (the
`{Debits}`/`{Credits}` args carry the configured currency symbol since
T024); dropped the stale FR-034 doc-comment reference.

Tests: 5 new Reports provider tests + V28_StatementIntegrityTests (5
acceptance cases). Full suite 1903 pass, build 0W/0E.



* chore(028): sync companion after_implement context (Phase 5 / US3)



* feat(028): US4 basis-of-accounting disclosure on financial statements (T041-T047)

ReportData gains an optional BasisOfAccounting string. PdfReportRenderer renders
it as a grey line below the "Generated:" line; CsvReportExporter appends it as a
trailing self-labelled note record after the grand total; ReportViewer shows it
in a <p> directly beneath the subtitle — each only when non-null.

New shared Reports_Common_BasisOfAccounting (neutral / en-US / fr-FR) with hybrid
wording that names both the accrual treatment of member fees and the cash
treatment of all other income and expenditure — never a single blanket basis
(FR-012). All eight financial-statement providers (Income Statement, Balance
Sheet, Trial Balance, Tax Summary — both return paths, Account Register, General
Ledger, Bank Reconciliation, Member Account Summary) set it; Member List and
Committee leave it null.

Tests: BasisOfAccountingTests (Reports), ReportViewerBasisTests (UI/bUnit),
V28_BasisOfAccountingDisclosureTests (integration acceptance, US4 AC-1..AC-2).
reports-contract.md CSV rendering row synced to the shipped single-column note.

Build 0W/0E; touched-project suites green (Reports 235, UI 613, Integration 215,
Localization 25).



* feat(028): US5 conventional bank reconciliation (T048-T051)

Rewrite BankReconciliationReportProvider to the standard adjusted-balance
layout — one section per bank account: balance per bank statement, add
outstanding deposits (listed + summed), less outstanding payments
(listed + summed), adjusted bank balance, balance per general ledger
(GetAccountBalanceAsync at the statement date), and a Reconciled residual
row that is zero on a tied statement (FR-013…FR-015, SC-008). All amounts
route through MoneyFormatter.

- ReportsResource neutral/en-US/fr-FR/qps-ploc: add BalancePerBankStatement,
  AddOutstandingDeposits, LessOutstandingPayments, AdjustedBankBalance,
  BalancePerGeneralLedger, Reconciled; drop the 10 now-dead cleared/summary
  keys (provider was their only consumer).
- BankReconciliationServiceTests: +2 FR-015 tests (finalise rejected over
  the half-cent tolerance with no state change; finalised reconciliation
  rejects toggle-clear and re-finalise).
- V28_ConventionalBankReconciliationTests: new integration acceptance
  (real SQLite + migrations) for US5 AC-1…AC-3.
- Sync contracts/reports-contract.md to the shipped layout.

Full suite green: Core 684, Reports 236, Localization 25, UI 613,
Data 154, Integration 220. Build 0W/0E.



* chore(028): sync companion after_implement context (Phase 7 / US5)



* feat(028): US6 closed-period lock (T052-T064)

Reject any financial transaction dated on or before Settings.ClosedThroughDate
so a reported prior period cannot be altered by a back-dated entry (FR-016/017),
while first-run setup opening balances stay permitted (FR-018).

- ClosedPeriodException (sealed, §5.2 shape) + IClosedPeriodGuard / ClosedPeriodGuard
  (reads the Settings singleton; no-op pre-setup or while nothing is closed).
- GLRepository consults the guard on the earliest line date before the write, at
  the single GL choke point; AddPairAsync is covered by delegation. UnitOfWork
  passes ClosedPeriodException through unwrapped like GLBalanceException, so a
  rejection rolls the whole operation back — no business row, no ledger line.
- General settings tab gains a "close periods through <date>" control
  (#settings-close-through-date) + a confirmation checkbox; the closed-through
  date only advances when both are set. SettingsRepository.SaveAsync now maps
  ClosedThroughDate on the detached-update path.
- All 7 finance posting forms catch ClosedPeriodException and show
  Validation_ClosedPeriod_PostingRejected, staying re-submittable.
- Tests: ClosedPeriodGuard unit (boundary/null cases), ClosedPeriodLock
  integration sweep (journal/expense/income/deposit/payment/fee-accrual/
  forgiveness all reject in-period and post after), SetupService FR-018,
  ClosePeriodControl bUnit. 57 GLRepository test construction sites updated
  for the new required guard dependency.

Build 0W/0E; suite green (Core 691, Integration 235, Data 154, UI 616,
Reports 236, Localization 25) — one pre-existing random-GUID "fee" substring
flake in EventListTests, passes on re-run.



* chore(028): sync companion after_implement context (Phase 8 / US6)



* feat(028): US7 financial-year start as a setup decision (T065-T075)

FinancialYearCalculator.GetRange/GetPreviousRange gain an optional
startDay (default 1, preserving every existing caller); the year now
pivots on the (month, day) anchor. The four FY-preset report providers
(TrialBalance, BalanceSheet, IncomeStatement, TaxSummary) and
OpeningBalancesWizard pass settings.FinancialYearStartDay through.

SetupRequest / SetupFormModel / SetupService carry FinancialYearStartMonth
+ FinancialYearStartDay; SetupService validates the day to 1..28
(Validation_Setup_FinancialYearStartDayRange) and persists both. The
setup General tab gains mandatory month + day pickers
(#setup-fy-start-month / #setup-fy-start-day) and the Settings General
tab gains a matching day picker beside the existing month one.

Tests: FinancialYearCalculatorTests (non-first-of-month / February
anchors, UTC end-of-day, default-to-1 equivalence), FinancialYearStartTests
(integration — setup persists a non-first-of-month start; every FY-preset
report honours the anchor; AU July-1 dataset unchanged),
FinancialYearStartPickerTests (bUnit), and SetupServiceTests additions.
Build 0W/0E; 1986 tests pass.

T076 (FR-022 sub-twelve-month first-year follow-on issue) is recorded in
spec.md Assumptions; the GitHub issue itself still needs to be created.



* chore(028): sync companion after_implement context (Phase 9 / US7)



* chore(028): companion after_implement task-sync (75/94)



* feat(028): US8 audit retention, coverage & purge-failure surfacing (T077-T083)

Phase 10 of spec 028 (International accounting-practice readiness), User Story 8:

- T080: raise the audit-retention default 1 -> 5 at the remaining declaration
  sites (SetupFormModel, SetupRequest); SetupService flows it through unchanged.
  Entity/config/migration defaults were already 5 (T003/T005/T006).
- T081: AttendanceService.RecordBatchAsync now writes an AuditTrailEntry for the
  attendance-fee accrual and, when paid at creation, the auto-payment - inside
  the existing UnitOfWork transaction (FR-026).
- T082: a failed startup audit purge is no longer swallowed.
  AuditTrailService.PurgeOlderThanAsync propagates; IStartupDiagnosticService
  gains a non-fatal warning channel (HasStartupWarning / StartupWarning /
  RecordWarning); MauiProgram's purge catch logs AND records the warning, and
  the retention fallback follows the new 5-year default. Startup still continues
  and the user is not routed to the blocking recovery page (FR-025).
- T083: a dismissible Dashboard banner surfaces the warning
  (Shared_StartupWarning_* in neutral / en-US / fr-FR).

Tests: T077 migration default (fresh -> 5, existing value preserved),
T078 AttendanceService audit coverage, T079 purge-failure propagation +
diagnostic surfacing, plus 3 Dashboard banner bUnit tests. Reconciled the two
SetupService default-flow tests and AuditTrailServiceTests to the new behaviour.
data-model.md section 10 updated. Full build 0W/0E; 1999 tests pass.



* docs(028): US9 accounting policies + living-spec currency (T084-T086a)

Phase 11 / User Story 9 — "Have the accounting policies written down":

- T084: new docs/accounting-policies.md — basis of accounting (hybrid
  accrual/cash), revenue recognition, rounding to the currency minor unit,
  single-currency policy, record immutability + reversing-entry corrections,
  period locking, financial year, audit-trail retention; states the reports
  are unaudited management accounts (FR-027, SC-012).
- T085: capabilities/finance/spec.md de-drafted; every retired
  registration-based tax reference (IsGstRegistered, per-fee GstCode,
  gross ÷ 11, "GST Collected", "GST clearing accounts", "net of GST")
  rewritten to the spec-016 model — Settings.IsTaxApplicable / TaxRate /
  TaxCode (Taxable, TaxExempt, Excluded), Tax Collected account 2310
  (FR-028).
- T086: capabilities/audit-trail/spec.md retention figure corrected from a
  fixed 12 months to a configurable 1-7 years defaulting to 5 on new
  datasets, existing values preserved (stale after T003's default change).
- T086a: spec.md Assumptions notes the pre-existing stale
  capabilities/settings/spec.md ABN/GST wording as a carried-forward
  follow-up, outside FR-028's finance-only scope.

Markdown-only phase — no code change; full rebuild + Localization.Tests
run in Phase 13 (T090/T091). Deferred T076 and Phases 12-13 remain.



* chore(028): companion after_implement task-sync (85/94)



* docs(028): US10 sales-tax internationalisation assessment spike (T087, T089)

Phase 12 / User Story 10 (P3, spike) — FR-029, FR-030, FR-033.

- T087: new docs/assessments/sales-tax-internationalisation.md — for each of the
  four required points, an in-scope/out-of-scope decision with a rough size:
  rate history/effective-dating OUT (L); tax-exclusive entry IN (M);
  balance-sheet classification of recoverable input tax (accounts 2310/2320) IN
  (S–M); multiple simultaneous rates/jurisdictions OUT (XL). The two in-scope
  points carry full ready-to-file follow-on issue specs.
- T089: FR-033 verification — git diff master...HEAD over every tax-adjacent file
  confirms the only tax-path change on this branch is the optional minorUnitDigits
  rounding param on TaxCalculator.SplitInclusive (default 2 → AUD byte-identical).
  TaxCode enum, 2310/2320 accounts, GL line structure, tax-inclusive entry model
  and Tax Summary net arithmetic are untouched; AudZeroDriftTests stored-value
  assertions hold. Recorded in spec.md Assumptions.
- T088 left unchecked: `gh issue create` is blocked by this environment's action
  classifier (same as T076). The two follow-on issues are specified verbatim in
  the assessment and noted in spec.md Assumptions for the maintainer to file.

No code changed — Markdown only.



* test(028): Phase 13 polish — currency-symbol guard + SC evidence (T090-T094)

Phase 13 / Polish & cross-cutting validation (spec 028). Completes the
implementation bar the two follow-on GitHub issues (T076, T088), which
gh issue create cannot file in this environment — their full text is in
spec.md Assumptions + docs/assessments/sales-tax-internationalisation.md.

- T090: full dotnet build -t:Rebuild = 0W/0E; full dotnet test = 2002 pass
  (Core 714, Localization 28, Reports 236, UI 624, Data 156, Integration 244).
- T091a: new CurrencySymbolGuardTests — repo-wide guard that no Reports
  provider and no UI money-display site hard-codes a "$"/"AUD" literal or
  formats money with ToString("C")/"{0:C}"/ToString("F2"); the 9 money-bearing
  providers each route through MoneyFormatter (FR-004, SC-002).
- T091: StageFright.Localization.Tests green (28/28) incl. the new guard.
- T092: CLAUDE.md Reports-pipeline money note (MoneyFormatter for every
  provider/renderer/exporter amount); finalised the remaining stale
  specs/027-localization-support/spec.md FR-015 lines (Clarifications, Edge
  case, FR-015 itself) with "Superseded by spec 028" notes, matching the
  T028 precedent.
- T093: re-ran the AUD zero-drift regression against the final build —
  AudZeroDriftTests + V28_CurrencyConfigurationTests 9/9 pass; identical
  report figures and stored monetary/tax/GL values (SC-004, FR-031, FR-032).
- T094: walked SC-001..SC-013 — every criterion maps to a green V28_*
  acceptance scenario or InternationalAccounting integration test (52/52
  pass) for a non-AUD, non-first-of-month org end to end; recorded
  specs/028-international-accounting-standards/sc-verification-evidence.md.

Tasks: 92/94 checked. T076 + T088 stay open pending maintainer issue filing.



* chore(028): companion after_implement task-sync (92/94)



* chore(028): file follow-on issues #353-#356, close #341 + sub-issues; mark spec complete

gh issue create is now permitted (global settings allow rule), so the two
follow-on-issue tasks left open in Phase 13 are done:

- Filed against parent #341: #353 (FR-022 sub-twelve-month first FY, T076),
  #354 (tax-exclusive entry) and #355 (recoverable input tax 2320
  classification) from the US10 spike (T088), and #356 (stale
  capabilities/settings/spec.md tax wording, T086a).
- Closed #341 and all eleven sub-issues #342-#352, each with a comment
  naming the delivering user story / tasks. Delivery is on this branch,
  pending merge to master.
- spec.md Assumptions, docs/assessments/sales-tax-internationalisation.md
  (_pending_ -> #354/#355) and sc-verification-evidence.md updated with the
  filed issue numbers.
- T076 + T088 checked -> 94/94 tasks; write-context.py --mark-complete run,
  spec 028 now status: completed. --fold-living-spec is a clean no-op
  (US9 edited the finance/audit-trail living specs in place).



* feat(028): sub-twelve-month first financial year, labelled a part-year (#353)

Implements FR-022 / issue #353 as spec 028 Phase 14, on the existing
028 branch (no new branch).

- Settings.InceptionDate (DateTime?, nullable, default null) — optional
  organisation founding date, captured at first-run setup; migration
  AddOrganisationInceptionDate (TEXT NULL).
- FinancialYearCalculator gains first-period-aware GetRange/GetPreviousRange
  overloads taking DateTime? inceptionDate and returning
  (From, To, bool IsPartYear). A null inception, or one on the anchor,
  reproduces the existing 12-month behaviour exactly.
- TrialBalance / IncomeStatement / TaxSummary / BalanceSheet providers pass
  settings.InceptionDate and wrap their subtitle via the new
  Reports_Common_PartYearSubtitle when the default FY-preset period is the
  part-year first period (never for a user-supplied custom range).
- Setup: optional #setup-inception-date <InputDate> on GeneralAppearanceTab;
  SetupFormModel/SetupRequest/SetupService plumb it through.
- Resources: Reports_Common_PartYearSubtitle and
  Setup_General_InceptionDate{Label,Help} in neutral/en-US/fr-FR;
  qps-ploc regenerated (also catches pre-existing spec-028 drift).
- spec.md (FR-022 delivered, SC-014, Assumptions, Edge Case, Key Entity)
  and data-model.md updated.

Presentation / range calculation only — no stored monetary, tax or GL
value changes; AudZeroDriftTests still green. Full rebuild 0W/0E;
2019 tests pass (+8 calculator, +5 integration, +4 bUnit).



* feat(028): tax-exclusive amount entry (net + tax), alongside tax-inclusive (#354)

Implements issue #354 as spec 028 Phase 15, on the existing 028 branch
(no new branch). The in-scope "Issue A" from the US10 sales-tax
internationalisation spike.

- TaxEntryMode enum (Inclusive — default, every pre-#354 dataset — or
  Exclusive) on Settings.TaxEntryMode; migration AddTaxEntryMode
  (TEXT NOT NULL DEFAULT 'Inclusive', string-converted like every other
  enum column), so an existing row backfills to Inclusive and stays
  byte-identical.
- TaxCalculator gains SplitExclusive(net, rate, digits) -> (gross, tax)
  with tax = round(net * rate / 100) and gross = net + tax, plus a
  Split(entered, mode, rate, digits) -> (gross, net, tax) dispatcher so
  every taxable posting service has one uniform call site.
- FeeService / IncomeEntryService / ExpensePaymentService /
  AttendanceService dispatch through TaxCalculator.Split: in Exclusive
  mode the entered figure is the net, tax is added on top, and the
  receivable/bank line (plus Fee.Amount, Payment.Amount and the
  attendance paid-at-creation cash pair) carry the gross while the
  income/expense line keeps the net. Inclusive mode is unchanged.
- ReactivationForgivenessService is deliberately untouched: it reverses
  a stored gross Fee.Amount, which SplitInclusive re-sums by
  construction, so the write-off stays balanced in either mode and no
  historical figure is reinterpreted.
- Setup: SetupFormModel / SetupRequest / SetupService plumb TaxEntryMode
  through; SalesTaxTab and TaxSettingsTab render an Inclusive/Exclusive
  selector while tax applies (reset to Inclusive on toggle-off);
  SettingsService.SaveAsync forces Inclusive when tax is off;
  GeneralSettingsTab merges TaxEntryMode from the fresh fetch.
- RecordIncome / ExpensePaymentPage: the tax hint picks
  Finance_Common_TaxExclusiveHint vs the existing inclusive hint, and
  the Amount label reflects the mode.
- Resources: Finance_Common_TaxExclusiveHint /
  AmountLabelTax{Inclusive,Exclusive}, Settings_/Setup_Tax_EntryModeLabel,
  Enum_TaxEntryMode_{Inclusive,Exclusive} in neutral / en-US / fr-FR;
  qps-ploc regenerated; Us2LocalizationGuardTests.UserFacingEnums gains
  typeof(TaxEntryMode).
- spec.md (Assumptions #354-delivered + FR-033 verification extended,
  SC-015), data-model.md (Settings.TaxEntryMode row, AddTaxEntryMode
  migration, Phase 15 section), the sales-tax assessment (Point 2
  delivered) and CLAUDE.md updated.

Additive and opt-in — no GL line-structure, 2310/2320 or TaxCode change;
Inclusive mode and AudZeroDriftTests are byte-identical. Full rebuild
0W/0E; 2053 tests pass (+28 calculator, +4 service, +5 integration,
+1 migration, +3 bUnit).



* feat(028): classify recoverable input tax (2320) as a Balance Sheet asset (#355)

Implements issue #355 as spec 028 Phase 16, on the existing 028 branch
(no new branch). The in-scope "Issue B" from the US10 sales-tax
internationalisation spike (assessment Point 3), option 1 (preferred).

- Seed: the system account 2320 is re-typed from AccountType.Liability
  "Tax Paid" to AccountType.Asset "Tax Receivable" — tax paid on
  purchases is recoverable from the tax authority, so it is an asset
  (a receivable). 2310 "Tax Collected" (owed to the authority) stays a
  Liability.
- Migration ReclassifyInputTaxAsReceivable: UpdateData on Name + Type of
  the seed row; Down restores "Tax Paid" / "Liability". Paired Designer
  and model snapshot updated. No column added or dropped.
- Number kept as 2320, a documented asset exception. Renumbering into
  the asset range would desync the denormalised Transaction.GLAccount
  string snapshot on every historical ledger row.
  GetNextAccountNumberAsync is unaffected (it already excludes system
  accounts and 2320 is outside its 1000-1999 asset scan window).
- No provider code change. BalanceSheetReportProvider and
  TrialBalanceReportProvider section purely by AccountType, so a
  net-refundable org's recoverable tax now presents under Assets
  (debit-normal, positive) and a net-payable org's tax owed under
  Liabilities via 2310, with the Trial Balance still tying exactly.
- TaxSummaryReportProvider unchanged: taxOnPurchases / net are computed
  from directional GL movements (GetAccountMovementsAsync), not the
  account's classification, so the sign convention (net = tax on sales
  - tax on purchases) needed no flip — only a comment refresh.
- OpeningBalanceService unchanged in code — ToNormalSide keys off
  account.Type, so a positive carried-over balance for 2320 now posts
  debit-normal. OpeningBalanceServiceTests updated to match.
- SystemAccounts / AccountNumberAssignmentService / Account doc-comments
  refreshed to "Tax Receivable"; the SystemAccounts.TaxPaid* C#
  identifiers are retained for continuity.
- Tests: ReclassifyInputTaxAsReceivableMigrationTests (pre-migration
  Liability/"Tax Paid" -> post Asset/"Tax Receivable", number kept);
  RecoverableInputTaxClassificationTests (net-refundable -> Assets;
  net-payable -> Liabilities; Trial Balance ties; exactly 3 sections).
- spec.md (Assumptions #355-delivered bullet, FR-033 verification
  extended, SC-016, Verbatim Constraints), data-model.md (intro,
  migration entry, new section 13), the sales-tax assessment (Point 3 /
  Issue B / Summary / filing status marked delivered) and CLAUDE.md
  updated.

Classification and presentation only — no stored monetary amount, no
TaxCode value, no ledger line moves. Full rebuild 0W/0E; 2057 tests
pass (+2 migration/integration test files), AudZeroDriftTests
byte-identical.



* docs(028): retire ABN/GST wording from settings living spec (#356)

capabilities/settings/spec.md still described the retired registration-based
tax model (IsGstRegistered, per-fee GstCode, a "GST / BAS" tab, the ATO ABN
checksum). Spec 016 replaced that with Settings.IsTaxApplicable / TaxRate /
AnnualFeeTaxCode / AttendanceFeeTaxCode, and spec 028 (#354) added
TaxEntryMode; the settings living spec had been stale since spec 016 because
028 FR-028 scoped only the finance living spec.

- Purpose: "GST treatment" -> "sales-tax treatment"; add currency + FY start.
- First-run validation requirement: drop ABN; list the real setup-time
  checks, incl. "tax rate > 0 only when sales tax applies", with a scenario.
- Remove the whole "ABN ... checksum-valid" requirement and its two
  scenarios (the Abn column was dropped in the GenericSalesTax migration).
- Independent-tabs requirement + scenarios: "GST/BAS tab" -> "Sales Tax tab".
- Replace "GST registration controls whether GST codes apply" with
  "Sales-tax applicability controls whether a rate and tax codes apply",
  matching SetupService.InitializeAsync and SettingsService.SaveAsync
  (force rate/codes null + TaxEntryMode=Inclusive when not applicable;
  reject rate <= 0 when applicable); resolves the stale NEEDS CLARIFICATION.

Docs-only; no code change.



* docs(028): retire ABN/GST wording across the other living specs (#356)

Follow-on to the settings living-spec cleanup: the same retired
registration-based tax vocabulary (spec 016's GstCode -> TaxCode /
IsGstRegistered -> IsTaxApplicable rename, and the "GST Collected/Paid"
account names) was still present in five more capability living specs.

- domain-model: `Transaction.GstCode` / `Fee.GstCode` -> `.TaxCode`;
  `Settings.IsGstRegistered` -> `IsTaxApplicable`; scenario retitled.
- data-access: seeded-system-account list "GST Collected/Paid" ->
  "Tax Collected, Tax Receivable" (current seed names; 2320 became
  "Tax Receivable" in the ReclassifyInputTaxAsReceivable migration, #355).
- app-host: setup-wizard requirement "GST treatment" -> "sales-tax
  treatment (applicability, rate, and per-fee tax codes)"; dropped the
  ABN-era "and tax details" from organisation identity.
- rehearsals: attendance accrual "plus GST Collected when the fee is
  taxable" -> "plus a Tax Collected credit when the fee is taxable and
  sales tax applies".
- reports-pipeline: GL-report example "BAS Summary" -> "Tax Summary"
  (the actual TaxSummaryReportProvider name).
- finance: same line also fixed a now-false claim — per
  OpeningBalanceService, only Opening Balance Equity is excluded as an
  opening-balance entry target; Member Receivable and the tax clearing
  accounts (2310/2320) are deliberately eligible so carried-over
  balances can be seeded. "Tax Paid 2320" -> "Tax Receivable 2320".

Docs-only; no code change.



* chore(028): seed a complete 2026 — rehearsals to year-end, 65% attendance on last 3

Advance the debug seeder's reference "today" from 27 Oct 2026 to 31 Dec 2026 so
both seeded calendar years are fully settled: every 2026 rehearsal now has
attendance recorded and its door cash banked, all annual fees are paid (bar the
two deliberate non-payers), and the year-end quarterly bank fee is posted —
nothing is left scheduled-but-unrecorded. The AGM is already dated late October
and the setup wizard's default currency/language (AUD, Australian English) are
unchanged.

The last three rehearsals of 2026 (early-to-mid December) now model an
end-of-year turnout dip: a flat 65% per-member attendance chance instead of each
member's usual 85–100% profile rate.

DEBUG-only fixture code (no test coverage, product behaviour untouched), so no
build/test run.



* chore(028): anchor seed data to the real run date so future dates stay unheld

The prior commit froze the seeder at 31 Dec 2026, which marked rehearsals that
have not happened yet as held — unrealistic. Anchor SeedCurrentDate to
DateTime.UtcNow.Date instead: every dated write in the seeder is already gated on
"> SeedCurrentDate", so future rehearsals, the spring concert and unpaid future
bills now stay scheduled-only whenever the sample data is generated.

- SeedRehearsalsAsync: the 65% turnout dip now applies to the three
  most-recently-*held* 2026 rehearsals (computed from heldCount), not the last
  three by calendar position; future rehearsals are scheduled with no attendance.
- SeedAgmAsync: always ScheduleAsync the AGM so a late-October record exists, but
  only RecordAsync (attendance + committee election) once its date has passed.
  Today the 2026 AGM is a scheduled, not-yet-held record.
- Doc-comments and the RNG comment updated to match.

DEBUG-only fixture code; not compiled or run (per instruction to skip build/test).



* docs: add Claude Code to the README tools-used list



* Add script to remove sample data

* chore(seed): generate organisation name and fee settings in the debug data seeder

The debug data seeder read Settings.OrganizationName / AnnualFee /
AttendanceFee from whatever the setup wizard captured. A coordinator who
left the fees at zero got a hollow sample dataset: SeedAnnualFeesAsync
was a no-op and the door-cash -> petty-cash -> bank sweep in
SeedRehearsalsAsync moved nothing.

SeedAsync now stamps a generated organisation name ("Clarence Valley
Community Choir") and fee schedule -- annual $120, per-rehearsal $2,
February membership renewal, October committee renewal, six-seat general
committee -- over the wizard inputs before it uses them, mirroring how
sample data already supplies its own accounts, opening balances and
committee/AGM history (spec 022). Currency, language and sales-tax
treatment stay as configured. The ISettingsService.SaveAsync audit entry
is suppressed by the enclosing AuditTrailSuppressionScope like every
other seeded write.

Updates the DebugDataSeeder class summary and the app-host living spec's
"Optional sample-data seeding" section.



* fix: point delete-database.cmd at the real MAUI app-data path

The script deleted nothing: it targeted %AppData% (Roaming) with
forward-slash separators, which del misparses as switches. The MAUI
app writes stagefright.db to FileSystem.AppDataDirectory, which on the
unpackaged Windows head resolves to
%LOCALAPPDATA%\StageFright Community\com.stagefright.community\Data.

Rewrite the script to:
- use %LOCALAPPDATA% with backslash separators and quoted paths
- remove the SQLite WAL sidecars (stagefright.db-wal / -shm) too
- also clear a repo-root design_time.db left by "dotnet ef" runs
- guard every delete with "if exist" and report whether anything went

Also note the concrete Windows path and the script in docs/SETUP.md's
"Reset the Database" section.



* fix: language selector only offered Australian English (#360)

The Settings ▸ General and Setup Wizard display-language pickers listed
only "English (Australia)" even though the app now ships en-US and fr-FR
satellite resource sets (added on top of spec 027).

Root cause: MauiProgram registered the catalog as
`AddSingleton<ISupportedLanguagesCatalog, SupportedLanguagesCatalog>()`.
Microsoft.Extensions.DependencyInjection selects the greediest resolvable
constructor, so it picked `SupportedLanguagesCatalog(IEnumerable<string>
resourceAssemblyNames)` — the test-only seam — and resolved the
unregistered `IEnumerable<string>` as an *empty* sequence (not null).
The constructor's `?? DefaultResourceAssemblyNames` guard only covered
null, so `_resourceAssemblyNames` became `[]`, `ContainsOurSatellite`
probed for nothing, no `<culture>/StageFright.*.resources.dll` folder
ever matched, and only the hard-coded `en-AU` baseline survived. It was
masked until en-US/fr-FR shipped because en-AU was previously the only
set anyway; every test used `new SupportedLanguagesCatalog()` directly or
a fake catalog, so none exercised the DI path.

- MauiProgram: register with an explicit factory
  (`_ => new SupportedLanguagesCatalog()`) so the documented parameterless
  constructor is used.
- SupportedLanguagesCatalog: the `(IEnumerable<string>)` constructor now
  treats an empty sequence like null and falls back to probing
  Core/UI/Reports — defence in depth against any DI/empty-list caller.
- Tests: SupportedLanguagesCatalogTests gains an empty-list fallback case;
  StartupSequenceTests gains a DI-container regression that resolves the
  catalog the "natural" `AddSingleton<TService,TImpl>()` way and asserts
  en-US and fr-FR are discovered (both fail without the fix, reproducing
  the "en-AU only" list).
- Refreshed three now-stale "en-AU is the only shipped set" comments in
  the picker area (GeneralSettingsTab, LanguagePickerRenderTests,
  V21_LocalizationStartupTests).

Verified in the running MAUI app via CDP: the Setup Wizard language
select now offers en-AU / en-US / fr-FR. Full rebuild 0W/0E; 2059 tests
pass.



* docs(029): spec — first-run language selection & optional sample-data seeding (#361)

New spec-kit feature on branch 029-first-run-language-seed. Addresses issue
#361 (a saved language change is invisible until restart, with no prompt) and
folds in a requested extension: move the Debug-only sample-data choice onto the
same pre-wizard screen and strip both the language and sample-data steps out of
the setup wizard.

Scope captured in spec.md:
- P1: a dedicated first-run language screen before the setup wizard; the choice
  is persisted outside the database and applied by one automatic restart.
- P2: Settings language change shows a post-save "restart now / later" dialog
  instead of the transient inline notice.
- P3 (Debug only): a "load sample data" option on the first-run screen that
  seeds before restarting and lands o…

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@SteveTeece
SteveTeece merged commit 04f6c75 into master Sep 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant