Skip to content

Fix/mac prerelease universal rids - #373

Closed
SteveTeece wants to merge 83 commits into
masterfrom
fix/mac-prerelease-universal-rids
Closed

SteveTeece wants to merge 83 commits into
masterfrom
fix/mac-prerelease-universal-rids

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 28 commits August 30, 2026 14:34
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
International accounting-practice readiness (spec 028), together with the
spec 027 localization work it was branched from and which had not yet
reached dev.

spec 027 — full IStringLocalizer-based localization: en-AU baseline plus
en-US and fr-FR resource sets, OS-aware persisted display language, and a
repo-wide hard-coded-literal guard.

spec 028 — configurable ISO 4217 currency driven through MoneyFormatter;
locale-safe money entry; Balance Sheet / Trial Balance integrity guards;
basis-of-accounting disclosure on financial statements; conventional bank
reconciliation; closed-period lock; financial-year start as a first-run
setup decision; audit-trail retention, coverage and purge-failure
surfacing; tax-exclusive amount entry alongside tax-inclusive;
sub-twelve-month first financial year labelled a part-year; recoverable
input tax (2320) reclassified as a Balance Sheet asset.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… 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>
@SteveTeece

Copy link
Copy Markdown
Owner Author

Incorrectly attempted to merge to maser. Should be merged to dev.

@SteveTeece SteveTeece closed this Sep 2, 2026
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