Regional Defaults¶
On-demand reference. The short rule in CLAUDE.md is: no default may privilege a single region, currency, or locale. Every user-facing format resolves from user/company setting -> browser/system -> neutral fallback. This page is the enforcement-script reference and per-line opt-out guide.
Resolution chain¶
| Surface | Source order |
|---|---|
| Currency (backend) | budget.currency runtime setting > DEFAULT_CURRENCY from synthorg.budget.currency |
| Currency (frontend) | useSettingsStore().currency > DEFAULT_CURRENCY from @/utils/currencies |
| Locale (backend) | None: the backend does no locale-aware number/date formatting. It stores and returns raw UTC ISO 8601 timestamps and unformatted numeric values; all locale-aware rendering happens in the frontend. No operator-tunable backend locale setting exists (the company name_locales list controls procedural-name generation, not number / date formatting). |
| Locale (frontend) | getLocale() from @/utils/locale > browser default > 'en' |
| Timezone | UTC stored everywhere; render via Intl without passing a timeZone option (browser tz wins) |
| Number / date format | Intl with the resolved locale; never hand-rolled templates |
| Units | Metric only |
| Spelling | International / British English UI default (colour, behaviour, organise, centred, analyse, cancelled); document deviations |
What's enforced¶
Two PostToolUse hooks run on every Claude Code edit; one CI gate runs in pre-push and GitHub Actions for non-Claude commits.
Frontend: scripts/check_web_design_system.py¶
PostToolUse hook on every web/src/ Edit/Write. Flags:
- Hardcoded ISO 4217 currency codes (
'USD','EUR','GBP', ...) outside the allowlisted files (web/src/utils/currencies.tsand theDEFAULT_CURRENCYre-export informat.ts). - Currency symbols adjacent to digits in user-facing strings (
"$10","€50"). - Identifiers ending in
_usd(the field carries the operator's configured currency; the type carries money semantics, not the name). - BCP 47 locale literals (
'en-US','de-DE', ...) anywhere in production code. - Bare
.toLocaleString()/.toLocaleDateString()/.toLocaleTimeString()calls; use the helpers in@/utils/formatwhich all readgetLocale(). localhost:<port>in application code.
Hardcoded hex colours, font-family literals, pixel spacing, Motion durations, and missing Storybook stories are also flagged by this same hook (see web/CLAUDE.md).
Backend: scripts/check_backend_regional_defaults.py¶
PostToolUse hook on every src/synthorg/ Edit/Write. Flags:
- Hardcoded ISO 4217 currency codes outside the allowlisted file (
budget/currency.pysymbol table). - Currency symbols adjacent to digits in user-facing strings.
- Identifiers ending in
_usd(backend, API DTOs, Pydantic types, DB columns). - BCP 47 locale literals.
localhost:<port>in application code.
CI gate: scripts/check_forbidden_literals.py¶
Runs in pre-push and GitHub Actions. Same detection set as the two PostToolUse hooks but covers diffs from non-Claude commits (manual git, agents that bypass the hook, Renovate, ...). The pre-push hook fails the push if any new occurrence is introduced; the CI job fails the build.
CI gate: scripts/check_currency_aggregation_invariant.py¶
Runs in pre-push. AST-walks src/synthorg/ for unguarded aggregations over currency-bearing attributes (cost, amount, total_cost, usd, eur) -- specifically sum(...), math.fsum(...), statistics.mean(...), statistics.fmean(...), and bare-name imports of fsum / mean / fmean (e.g. from statistics import mean) whose first argument is a generator / list / set comprehension whose element walks to one of those attributes (the detector matches wrapped forms like abs(r.cost) or r.cost or 0.0, not only bare attribute access). A preceding call to assert_currencies_match (or the legacy assert_single_currency) in the same enclosing function or module scope clears the violation; guards inside nested def / class / lambda bodies do NOT count for an outer aggregation. Per-line opt-out: # lint-allow: currency-aggregation -- <reason> (mandatory non-empty justification). The gate's job is to catch the FIRST regression after the convention has been rolled out; the helper itself lives in synthorg.budget.currency.
Per-line opt-out¶
Legitimate opt-outs use one of:
- Python:
# lint-allow: regional-defaults - TypeScript:
// lint-allow: regional-defaults
The marker can be placed on the offending line or the line directly above. It opts out of all four detection categories on that line; do not stack multiple markers per line.
For the currency-aggregation invariant the marker is separate: # lint-allow: currency-aggregation -- <reason> with a mandatory non-empty justification.
Allowlisted files¶
These files contain the canonical literals and are exempted from currency / locale literal detection:
src/synthorg/budget/currency.py: symbol table +DEFAULT_CURRENCY.web/src/utils/currencies.ts: dropdown options +DEFAULT_CURRENCY.web/src/utils/format.ts:DEFAULT_CURRENCYre-export.web/public/provider-logos/*.svg: provider logo filenames mirror provider preset names by necessity.
Monetary models invariant¶
Every cost-bearing Pydantic model carries currency: CurrencyCode (ISO 4217, validated against the allowlist in synthorg.budget.currency), including CostRecord, TaskMetricRecord, AgentRuntimeState, ProjectCostAggregate, and DeliverableReceipt; several derived report/response models also carry it for display (grep -rn "currency: CurrencyCode" src/synthorg/ is the authoritative full list).
Every aggregation site enforces a same-currency invariant via assert_currencies_match; mixing currencies raises MixedCurrencyAggregationError (HTTP 409, error code 4007). The aggregation sites span the budget package (the cost tracker, report generation, forecasting, the optimiser, call and category analytics, trend analysis) and HR WindowMetrics.
See also¶
- errors.md §"Conflict (4xxx)" --
MIXED_CURRENCY_AGGREGATION(4007). web/CLAUDE.mdDesign System section -- the design-token side of the same hook.