toga-ai 1.0.413 → 1.0.415

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@
22
22
  | [_Model::save() vs raw _Query — no atomic conditional update](features/model-save-vs-query-atomic-update.md) | `_Model::save()` is a plain load-then-write ORM primitive and **cannot express an atomic conditional update** (an optimistic-concurrency / row-claim guard such | _underscore/Model.php, _underscore/Query.php |
23
23
  | [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
24
24
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
25
+ | [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22a - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22b - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22a - PersonaTranslationsFrench.sql |
25
26
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
26
27
  | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
27
28
  | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
@@ -0,0 +1,146 @@
1
+ ---
2
+ title: Persona Name Translation (PersonaTranslations sidecar)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: draft
9
+ updated: 2026-07-22
10
+ owners: ["tcox"]
11
+ files:
12
+ - _underscore/Model/Client/PersonaTranslation.php
13
+ - dbchanges2/Client/2026-07-22a - PersonaTranslations.sql
14
+ - dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql
15
+ - dbchanges2/Client/2026-07-22b - PersonaTranslationsAcl.sql
16
+ - dbchanges2/Client_CompassCanada/2026-07-22a - PersonaTranslationsFrench.sql
17
+ related:
18
+ - ./assortment-name-translation.md
19
+ - ../../api2/features/language-translation-layer.md
20
+ - ./acl-permission-chain.md
21
+ - ../../toga2-commerce/features/client-fields.md
22
+ - ../../../clients/compass-canada/profile.md
23
+ ---
24
+
25
+ ## Summary
26
+
27
+ Serves Persona **names** in multiple languages by adding a per-language **sidecar** table
28
+ `PersonaTranslations`, reusing the platform's existing metadata-driven translation mechanism —
29
+ built as an exact copy of the [Assortment Name Translation](./assortment-name-translation.md)
30
+ pattern. English stays in `Personas.name`; the sidecar holds only non-English overlays (null →
31
+ English fallback). Because the translation layer is fully metadata-driven (see
32
+ [Language Translation Layer](../../api2/features/language-translation-layer.md)), wiring this up
33
+ required **no api2 PHP logic change**: `GET /personas` names flow through `getTranslatedFieldValue`
34
+ at the top-level serialization sites. Compass Canada's French (fr-CA) persona names are the first
35
+ (and so far only) intended consumer.
36
+
37
+ > **Status (2026-07-22): DRAFTED / DEFERRED — not committed, not run.** All five artifacts live in
38
+ > local working trees only; the developer explicitly deferred the backend/DB changes this session.
39
+ > `sql-reviewer` verdict was **SAFE TO MERGE (0 critical)**. See **Rollout / pre-run verification**
40
+ > before executing — several ids and row-matches are *assumed* and must be confirmed against the live
41
+ > DB first. DB verification could not be completed this session: all RDS endpoints (prod readers,
42
+ > global, sandbox) are VPC-restricted from the dev machine — needs VPN or the claude.ai TOGa DB
43
+ > connector.
44
+
45
+ ## Key files / entry points
46
+
47
+ - `_underscore/Model/Client/PersonaTranslation.php` — the sidecar model
48
+ (`_Model_Client_PersonaTranslation`), a declarative copy of `AssortmentTranslation.php`.
49
+ - `dbchanges2/Client/2026-07-22a - PersonaTranslations.sql` — the sidecar table (every client DB).
50
+ - `dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql` — Core record + fields + the field link.
51
+ - `dbchanges2/Client/2026-07-22b - PersonaTranslationsAcl.sql` — Base-role ACL grant (every client DB).
52
+ - `dbchanges2/Client_CompassCanada/2026-07-22a - PersonaTranslationsFrench.sql` — fr-CA seed.
53
+
54
+ ## How it works
55
+
56
+ The mechanism itself is the data-driven translation layer documented in
57
+ [Language Translation Layer](../../api2/features/language-translation-layer.md) — this feature only
58
+ **registers a new translatable field** into it (identical shape to the assortment sidecar):
59
+
60
+ 1. **Sidecar table.** `Client.PersonaTranslations` holds non-English overlays for persona names.
61
+ English remains in `Personas.name`; a null/absent sidecar row falls back to English.
62
+ `UNIQUE(personaId, languageId)`; collation `utf8mb4_unicode_ci` to match the translation-sidecar
63
+ family (`AssortmentTranslations`, `ItemTranslations`, the feature sidecars).
64
+ 2. **Model.** `_Model_Client_PersonaTranslation` is the ORM model for the sidecar (declarative copy
65
+ of `AssortmentTranslation.php`).
66
+ 3. **Core record registration.** `Core.Records` registers route `persona-translations` →
67
+ model `\_Model_Client_PersonaTranslation` at record **id 346** (assumed next-free), with
68
+ `Core.RecordFields` rows **2458–2462** for `id`, `uuid`, `personaId`, `languageId`, `name`, and an
69
+ `UPDATE ... JOIN` that sets `translationRecordFieldId` on the `Personas.name` RecordField.
70
+ 4. **The field link (what makes it translate automatically).** The translatable `Personas.name`
71
+ RecordField gets its `Core.RecordFields.translationRecordFieldId` set to the sidecar's `name`
72
+ RecordField. The api2 V2 layer reads `translationRecordFieldId` for every field and automatically
73
+ returns the sidecar value for the caller's resolved language (falling back to English when absent).
74
+ For persona names this works with **no api2 code change** because `GET /personas` name is emitted
75
+ only at top-level serialization sites, all of which run through `getTranslatedFieldValue`.
76
+ (Contrast the assortment sidecar, which additionally needed a `join=`/joined-select fix — persona
77
+ names are not surfaced via a joined-select read path, so that 5th-serialization-site concern does
78
+ not apply here.)
79
+ 5. **ACL.** A full ACL chain grants the **Base** role read/write on the new record. Because the
80
+ record's `aclDatabase = 'CLIENT'`, the grant lives in each client DB — see
81
+ [ACL Permission Chain](./acl-permission-chain.md).
82
+
83
+ ## Data model
84
+
85
+ - `Client.PersonaTranslations` — `id, uuid, dtCreated, dtUpdated, personaId (FK Personas),
86
+ languageId (FK Languages), name`; `UNIQUE(personaId, languageId)`; `utf8mb4_unicode_ci`. English
87
+ stays in `Personas.name`; sidecar holds only non-English overlays (null → English fallback).
88
+ - `Core.Records` **346** (assumed) = `persona-translations` (`aclDatabase = 'CLIENT'`);
89
+ `Core.RecordFields` **2458–2462** (assumed) for id/uuid/personaId/languageId/name.
90
+ - `Personas.name` RecordField → `translationRecordFieldId` points at the sidecar `name` RecordField.
91
+
92
+ ## Rollout / pre-run verification (do this BEFORE executing)
93
+
94
+ The ids and several row-matches in these migrations are **assumptions from a repo scan**, not
95
+ confirmed against the live DB. Before running:
96
+
97
+ 1. **Verify next-free Core ids.** Confirm live `MAX(Core.Records.id) = 345` and
98
+ `MAX(Core.RecordFields.id) = 2457` so that 346 and 2458–2462 are actually free. (Consistent with
99
+ the feature-translation work, which used Records 343/344/345 + RecordFields 2442–2457.) If the live
100
+ maxima differ, renumber the record/field ids in `PersonaTranslationsRecord.sql` and the ACL SQL.
101
+ 2. **Verify the 3 out-of-band personas.** The fr-CA seed matches 30 personas by known `Personas.uuid`
102
+ (from the 2025-12-19 Personas seed), plus **3 personas matched by exact English name** — *Admin
103
+ Canada*, *MacBooks*, *Managers/VIPs* — that exist only in the live DB (no repo migration). Confirm
104
+ those three exist with those exact names and that `Personas` has **no duplicate names** (the
105
+ name-match would double-seed otherwise).
106
+ 3. **Confirm the glossary is complete.** The business-glossary screenshot the fr-CA text came from was
107
+ cut off after "Managers/VIPs → Gestionnaires/VIP" — confirm there are no additional personas below
108
+ that line before treating the seed as complete.
109
+
110
+ fr-CA is `languageId 2` (same as the item-feature translations — see the Compass Canada profile and
111
+ `UserGlobalSettings.settingId = 2`).
112
+
113
+ ## Frontend consumption (toga2-commerce)
114
+
115
+ For the translated names to appear on the storefront **without a full reload** after a language
116
+ switch, the persona React Query caches must be language-sensitive. The persona query keys
117
+ `userPersonaDataOnLogin` and `userPersonaDataOnViewAs` were added to `LANGUAGE_SENSITIVE_QUERY_KEYS`
118
+ in `src/queries/queries.ts` (toga2-commerce) so they refetch on language change — see
119
+ [Client Fields](../../toga2-commerce/features/client-fields.md). This frontend change shipped this
120
+ session; the backend above did not.
121
+
122
+ ## Client variations
123
+
124
+ - **Compass Canada** is the intended first consumer: `2026-07-22a - PersonaTranslationsFrench.sql`
125
+ seeds fr-CA names for 33 personas (30 by uuid + 3 by exact English name). Deferred / not yet run —
126
+ see the Compass Canada profile.
127
+
128
+ ## Change history
129
+
130
+ - 2026-07-22 — Drafted the `PersonaTranslations` sidecar end-to-end following the
131
+ `AssortmentTranslations` pattern exactly: sidecar table + `_Model_Client_PersonaTranslation` model +
132
+ Core record 346 / RecordFields 2458–2462 registration + `translationRecordFieldId` link on
133
+ `Personas.name` + Base-role ACL chain + Compass Canada fr-CA seed (33 personas). `sql-reviewer` SAFE
134
+ TO MERGE (0 critical). **Not committed, not run** — ids and the 3 name-matched personas need live-DB
135
+ verification first (RDS was VPC-unreachable this session). No api2 code change required (persona
136
+ names serialize only at top-level `getTranslatedFieldValue` sites). (tcox)
137
+
138
+ ## Related docs
139
+
140
+ - [Assortment Name Translation](./assortment-name-translation.md) — the pattern this copies.
141
+ - [Language Translation Layer](../../api2/features/language-translation-layer.md)
142
+ - [ACL Permission Chain](./acl-permission-chain.md)
143
+ - [Client Fields](../../toga2-commerce/features/client-fields.md) — storefront language switching.
144
+ - [Compass Canada profile](../../../clients/compass-canada/profile.md)
145
+ </content>
146
+ </invoke>
@@ -3,5 +3,5 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
- | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql |
6
+ | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql |
7
7
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
@@ -62,6 +62,9 @@ files:
62
62
  - dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql
63
63
  - dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql
64
64
  - dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql
65
+ - dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql
66
+ - dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql
67
+ - dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql
65
68
  related:
66
69
  - ../../_underscore/features/surface-resolver.md
67
70
  ---
@@ -306,8 +309,31 @@ Both seeds are **ADDITIVE, id-agnostic, and re-runnable**: `NOT EXISTS`-guarded
306
309
  `config.role`/`valueKey`; Messages guarded by `messageKey`; the surfaces inherit `appId`/`recordId`
307
310
  from the existing `order-details` sibling surface **by slug** (never a hardcoded id). Same authoring
308
311
  pattern as the `2026-07-17g` seed. These are **Core→Core only** (prod-safe — no cross-DB Client
309
- subselect). Per-client divergence (via `SurfaceOverride`) for Compass / Compass Canada / Quad is **not
310
- yet seeded** — this session is the shared Core default + FE wiring only.
312
+ subselect).
313
+
314
+ ### Per-client decision-summary overrides (Quad hides + Compass/CC concat Total, 2026-07-21c)
315
+ The per-client divergence off the shared `sales-order-decision-summary` surface was seeded (all three
316
+ files use suffix `c`; `b` was already taken in each client folder, and each `run after Core/2026-07-21a`
317
+ per the RUN ORDER README). All are **client-level** (`roleId`/`personaId`/`languageId` NULL — see the
318
+ decision below), id-agnostic (resolved by `Surfaces.slug` + `config.valueKey`, never a hardcoded id),
319
+ and `NOT EXISTS`-guarded.
320
+
321
+ - **Quad field hides** (`Client_Quad/2026-07-21c`) — three `IS_VISIBLE='0'` overrides hiding **Assigned
322
+ Manager** (`assignedTo._name`), **Cost Center** (`c_erpEntityId`), and **Reason for Request**
323
+ (`reasonForRequest`, the read-only summary row — **NOT** the editable reason input). `IS_VISIBLE` is
324
+ in the base attribute ENUM (no widener needed). Renders end-to-end with **no FE change** (the resolver
325
+ applies visibility; the summary adapter already honors `el.isVisible`). Applied to local Client_Quad
326
+ and verified.
327
+ - **Compass + Compass Canada concatenated Total** (`Client_Compass/2026-07-21c`,
328
+ `Client_CompassCanada/2026-07-21c`) — a `CONFIG` override on the Total element setting the **full**
329
+ config `{"valueKey":"_totalPurchase","isConcatenated":{"valueKey":"_totalLease","suffix":"/mo",
330
+ "layout":"stacked","valueType":"currency"}}` — stacks the monthly lease figure under the purchase
331
+ Total for the lease clients. Because a `CONFIG` override **replaces the whole object** (see the resolver
332
+ doc), the override **must** carry the base `valueKey` (`_totalPurchase`), not just the added
333
+ `isConcatenated` key — omitting it silently drops the base binding. Resolved by slug +
334
+ `config.valueKey='_totalPurchase'`. Requires the small FE adapter extension to emit `isConcatenated`
335
+ (see [surface-frontend](../../toga25-supply/features/surface-frontend.md)). Authored this session;
336
+ only Quad confirmed applied locally.
311
337
 
312
338
  ## ThemeTokens — per-tenant, physically isolated in each client DB
313
339
 
@@ -473,6 +499,17 @@ rule resumes.
473
499
  override is added). **Open follow-up.**
474
500
 
475
501
  ## Change history
502
+ - 2026-07-21 — Seeded the **per-client decision-summary overrides** off the shared
503
+ `sales-order-decision-summary` surface (all `2026-07-21c`, client-level — `roleId`/`personaId`/
504
+ `languageId` NULL): **Quad** hides three summary rows (`assignedTo._name`, `c_erpEntityId`,
505
+ `reasonForRequest` — the read-only row, not the reason input) via `IS_VISIBLE='0'` (base ENUM, no FE
506
+ change); **Compass + Compass Canada** stack the monthly lease figure under Total via a `CONFIG`
507
+ override carrying the **full** config (`_totalPurchase` + `isConcatenated{_totalLease, suffix "/mo",
508
+ layout stacked, currency}`). All id-agnostic (slug + `config.valueKey`), NOT EXISTS-guarded, registered
509
+ in the RUN ORDER README (`run after Core/2026-07-21a`). Reinforces the CONFIG-replaces-wholesale rule:
510
+ a partial CONFIG override silently drops the base `valueKey`. Scoped client-level not role-scoped (a
511
+ display flourish for every tenant user; open follow-up to re-scope Compass concat to a role if wanted).
512
+ Only Quad confirmed applied locally. (apeterson)
476
513
  - 2026-07-21 — Seeded the **SalesOrder decision-modal (approve + deny) surfaces**, migrating the
477
514
  per-client `approvalActionFields.json` display config to metadata. `Core/2026-07-21a` = one SHARED
478
515
  `sales-order-decision-summary` SECTION surface (cardType `decisionSummary`, 8 read-only FIELD summary
@@ -6,7 +6,7 @@
6
6
  | [Cart Bundle Submission & the bundleUuid Identity Contract](features/cart-bundle-submission-and-identity.md) | How cart **bundles** (kits) are turned into `SalesOrderItems` when a cart is submitted or an existing order is edited, and the **identity-field contract** every | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts, src/utils/formatSalesOrderBundlesFromApi.ts, src/stores/useCartStoreZu.ts, src/pages/OrderDetails/helpers/formatSalesOrderDataFromLocalStorage.ts, src/pages/OrderDetails/view/components/OrderItems.tsx |
7
7
  | [Cart Notification Emails — duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/stores/useEmailOptionsStore.ts, src/stores/useCartSalesQuoteZu.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts |
8
8
  | [Cart Page — config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | The Cart page (`src/pages/Cart/`) is the most config-heavy page in `toga2-commerce`. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/pages/Cart/view/cartForm/CartFormSection.tsx, src/pages/Cart/view/cartForm/CartFormRenderer.tsx, src/pages/Cart/view/EditCart.tsx, src/pages/Cart/view/EditOrder.tsx, src/pages/Cart/viewModel/useEditOrderOrEditCartViewModel.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts, src/hooks/useAssignClientFields.ts |
9
- | [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/pages/Cart/api/CartApi.ts, src/hooks/useAuthenticationFlow.ts, src/contexts/AuthContext.tsx, src/pages/Login/viewModel/useLoginPageViewModel.ts, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx |
9
+ | [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/pages/Cart/api/CartApi.ts, src/hooks/useAuthenticationFlow.ts, src/contexts/AuthContext.tsx, src/pages/Login/viewModel/useLoginPageViewModel.ts, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx, src/components/NavIcons/NavIconItem.tsx, src/components/Submenus/AlertSubmenu.tsx, src/components/Submenus/types.ts, src/pages/Account/AccountPage.tsx, src/pages/Account/view/MyOrdersView.tsx, src/pages/GetSupport/GetSupportPage.tsx, src/pages/GetSupport/viewModel/useGetSupportViewModel.ts, src/queries/queries.ts |
10
10
  | [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | On the toga2-commerce **Cart** page, expedited shipping options (**"2nd Day EOB"** and **"Next Day Air"**) are only offered in the *Shipping Method* dropdown wh | toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/viewModel/FIELDS/shared/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/CartPage.tsx |
11
11
  | [Multi-Tenant Resolution & Theming](features/multi-tenant-theming.md) | `toga2-commerce` serves multiple clients from one codebase. | src/themeConfig/themes.json, src/themeConfig/ThemeContext.tsx, src/themeConfig/types.ts, src/components/ThemeSwitcher/ThemeSwitcher.tsx, src/components/AuthLayout/AuthLayout.tsx, src/api/axiosInstance.ts, src/contexts/AuthContext.tsx, tailwind.config.js |
12
12
  | [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-commerce` (React + Vite, "commerce2-react") builds and deploys on **AWS Amplify**. | toga2-commerce/amplify.yml, toga2-commerce/.gitattributes, toga2-commerce/package.json, toga2-commerce/.github/workflows/sync-stage-environments.yml |
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-15
9
+ updated: 2026-07-22
10
10
  owners: ["apeterson", "tcox"]
11
11
  files:
12
12
  - src/fieldsConfig/index.ts
@@ -22,11 +22,20 @@ files:
22
22
  - src/hooks/useDynamicConditionalFieldOptions.ts
23
23
  - src/stores/useFieldsStore.ts
24
24
  - src/components/BaseDetailField/BaseDetailField.tsx
25
+ - src/components/NavIcons/NavIconItem.tsx
26
+ - src/components/Submenus/AlertSubmenu.tsx
27
+ - src/components/Submenus/types.ts
28
+ - src/pages/Account/AccountPage.tsx
29
+ - src/pages/Account/view/MyOrdersView.tsx
30
+ - src/pages/GetSupport/GetSupportPage.tsx
31
+ - src/pages/GetSupport/viewModel/useGetSupportViewModel.ts
32
+ - src/queries/queries.ts
25
33
  related:
26
34
  - 2.0/apps/toga2-commerce/architecture.md
27
35
  - 2.0/apps/toga2-commerce/features/multi-tenant-theming.md
28
36
  - 2.0/apps/toga2-commerce/features/cart-notification-emails.md
29
37
  - 2.0/apps/toga2-commerce/features/cart-page-config-architecture.md
38
+ - 2.0/apps/_underscore/features/persona-name-translation.md
30
39
  ---
31
40
 
32
41
  ## Summary
@@ -141,7 +150,7 @@ export const FIELDS: Record<string, any> = {
141
150
  USER: { ... },
142
151
  },
143
152
  },
144
- COMPASSCANADA: { en: { ADMIN, SUPERUSER, MANAGER, USER }, fr: { ... } },
153
+ COMPASSCANADA: { en: { ADMIN, SUPERUSER, MANAGER, USER }, "fr-CA": { ... } },
145
154
  QUAD: { en: { GLOBALADMIN, BUYER, ITSHOPPER /*, USER */ } },
146
155
  };
147
156
  ```
@@ -156,8 +165,11 @@ export const FIELDS: Record<string, any> = {
156
165
  `ERRORFIELDS`, `LOGINSETTINGSFIELDS`, …).
157
166
 
158
167
  - **Tenant** keys: `COMPASS`, `COMPASSCANADA`, `QUAD`.
159
- - **Language** keys are **lowercase `en` / `fr`** in the registry. ⚠️ This differs from the on-disk
160
- **folder** names, which are uppercase `ENGLISH` / `FRENCH`. Only COMPASSCANADA has `fr`.
168
+ - **Language** keys are **`en` / `fr-CA`** in the registry (verified `src/fieldsConfig/index.ts`).
169
+ ⚠️ Two mismatches to keep straight: (1) the registry French key is **`fr-CA`**, *not* `fr`; and
170
+ (2) both differ from the on-disk **folder** names, which are uppercase `ENGLISH` / `FRENCH`. Only
171
+ COMPASSCANADA has `fr-CA`. See the "Language value & persistence" note and gotchas below — code that
172
+ compares the language must match `fr-CA` (use `startsWith("fr")`), never `=== "fr"`.
161
173
  - **Role** keys: COMPASS/COMPASSCANADA → `ADMIN`, `SUPERUSER`, `MANAGER`, `USER`;
162
174
  QUAD → `GLOBALADMIN`, `BUYER`, `ITSHOPPER` (+ `USER`).
163
175
 
@@ -195,7 +207,7 @@ const data = FIELDS?.[clientName]?.[language]?.[role] || null;
195
207
 
196
208
  Inputs:
197
209
  - **`clientName`** = `useFieldsStore.fieldKey` (the tenant `host`, set at login — see architecture doc).
198
- - **`language`** = `useFieldsStore.language` (default `"en"`; set to `"fr"` etc. via `setLanguage`
210
+ - **`language`** = `useFieldsStore.language` (default `"en"`; set to `"fr-CA"` via `setLanguage`
199
211
  from the login settings switcher or `MySettingsView`).
200
212
  - **`user`** = the logged-in user (role flags).
201
213
 
@@ -203,6 +215,16 @@ It returns `{ fields, loading, error, resetFieldsMapCache }`; `resetFieldsMapCac
203
215
  `["clientFields"]` query (used when role/language/tenant changes mid-session). Page ViewModels then
204
216
  pluck the section they need, e.g. `fieldsCall["MYACCOUNTFIELDS"]`.
205
217
 
218
+ ### Language value & persistence
219
+
220
+ `useFieldsStore.language` is the resolved registry language key — **`"en"` or `"fr-CA"`** for
221
+ COMPASSCANADA. It is persisted to `localStorage` via the zustand `persist` key **`"fields-key"`**
222
+ (`src/stores/useFieldsStore.ts`), so a chosen language survives reload. The Account settings save
223
+ (`MySettingsView`) stores the selected option's **uuid** (`"en"` / `"fr-CA"`) through `setLanguage`.
224
+ Any code that branches on the current language must compare against **`"fr-CA"`** — the safe test is
225
+ `language.startsWith("fr")`, **never `language === "fr"`** (that comparison silently fails and was the
226
+ root cause of the Get Support image bug — see gotchas).
227
+
206
228
  ## What's inside a FIELDS JSON
207
229
 
208
230
  Shape varies by page, but common forms:
@@ -235,14 +257,56 @@ Common field attributes: `uuid`, `label`, `valueKey` (path into the data object)
235
257
  - **New role:** extend the role-derivation in `useAssignClientFields.ts` **and** add the role key
236
258
  under each `FIELDS[tenant][lang]` branch + per-page role folders.
237
259
  - **New language:** set `languageSwitcher.isEnabled` in the tenant's `clientFields` JSON, add the
238
- `fr`/`xx` branch to `FIELDS[tenant]`, and the `<LANGUAGE>` on-disk folders.
260
+ `fr-CA`/`xx` branch to `FIELDS[tenant]` (use the exact registry key the switcher option's uuid
261
+ yields), and the `<LANGUAGE>` on-disk folders.
239
262
  - **Editing copy:** change the page's `FIELDS/<CLIENT>/[<LANG>/][<ROLE>/]<NAME>FIELDS.json`. Remember
240
263
  to update **all** affected role/language variants — there is no inheritance/fallback between roles.
241
264
 
265
+ ## Keying previously-hardcoded JSX through FIELDS (with an in-code English fallback)
266
+
267
+ When a piece of UI text was hard-coded in JSX and now needs to render French for COMPASSCANADA, the
268
+ pattern is: **add the key to the COMPASSCANADA FIELDS JSON only (EN + FR × every role), and read it in
269
+ the component with an in-code English literal fallback** — e.g. `fields?.alertsSubmenu?.markAllAsRead
270
+ ?? "Mark all as read"`. Because COMPASS/QUAD never get the key, they fall through to the English
271
+ literal and are **unaffected** — this is the deliberate way to add a COMPASSCANADA-only translation
272
+ without touching other tenants' JSON (distinct from the "no fallback" rule for whole role/section
273
+ lookups, which still holds). Examples shipped 2026-07-22 (all wired to fall back to English):
274
+
275
+ - **`HEADERFIELDS`** — new `adminSubmenu.manageRequests` ("Gérer les demandes") and an `alertsSubmenu`
276
+ group (`markAllAsRead` "Tout marquer comme lu", `openExternalLink` "Ouvrir TOGa Supply",
277
+ `alertSingular`/`alertPlural` "alerte"/"alertes", `allUpToDate` "Tout est à jour !"). Added to all
278
+ **8** COMPASSCANADA HEADERFIELDS files (EN+FR × 4 roles); consumed in
279
+ `src/components/NavIcons/NavIconItem.tsx` and `src/components/Submenus/AlertSubmenu.tsx` (+ `types.ts`).
280
+ - **`ORDERHISTORYFIELDS`** — Orders-for-Others page `ordersForOthersHeading` /
281
+ `ordersForOthersSubheading`, and the order-search box `pageSearchInput` ("Rechercher un numéro de
282
+ commande" / placeholder "Rechercher un nº de commande..."). Added to all 8 files; consumed in
283
+ `src/pages/Account/AccountPage.tsx` and `src/pages/Account/view/MyOrdersView.tsx`.
284
+ - **Get Support illustration is language-switched** (not a FIELDS key — an asset swap): the viewModel
285
+ (`src/pages/GetSupport/viewModel/useGetSupportViewModel.ts`) exposes `language`, and
286
+ `GetSupportPage.tsx` picks `assets/get-support-img-french.svg` when `language.startsWith("fr")`.
287
+
288
+ ## Language-sensitive React Query caches (`LANGUAGE_SENSITIVE_QUERY_KEYS`)
289
+
290
+ `FIELDS` covers static copy, but **server data** that itself comes back translated (e.g. persona names
291
+ via the [Persona Name Translation](../_underscore/features/persona-name-translation.md) sidecar) is
292
+ cached by React Query and does **not** refetch on a language switch by default. `src/queries/queries.ts`
293
+ keeps a `LANGUAGE_SENSITIVE_QUERY_KEYS` allow-list of query keys to invalidate/refetch when the
294
+ language changes. The persona query keys `userPersonaDataOnLogin` and `userPersonaDataOnViewAs` were
295
+ added to it 2026-07-22 so translated persona names appear after a language switch without a full page
296
+ reload. When you add a new API-driven string that must respond to the language switcher, add its query
297
+ key here.
298
+
242
299
  ## Gotchas
243
300
 
244
- - **`en`/`fr` (registry) vs `ENGLISH`/`FRENCH` (folders)** mismatching these is the classic bug;
245
- the runtime lookup uses lowercase.
301
+ - **Registry French key is `fr-CA`, not `fr`and never compare with `=== "fr"`.** The COMPASSCANADA
302
+ French registry key is `"fr-CA"`; any language branch must use `startsWith("fr")`. Comparing
303
+ `language === "fr"` silently never matches and shipped a real bug this session (Get Support kept the
304
+ English illustration in French mode). The doc previously mis-stated this key as `fr`.
305
+ - **`en`/`fr-CA` (registry) vs `ENGLISH`/`FRENCH` (folders)** — mismatching these is the classic bug;
306
+ the runtime lookup uses the registry keys, not the folder names.
307
+ - **The header "Admin" nav button stays "Admin" in French — do not "fix" it.** Decided 2026-07-22:
308
+ "Admin" is the natural French abbreviation of "administration", so the top-level button is left
309
+ untranslated on purpose (the dropdown *contents* are translated). It is not a localization gap.
246
310
  - **No fallback** in `FIELDS[client][language][role]` — a missing tenant/role/language combo returns
247
311
  `null` and the page renders empty. Keep all role variants in sync.
248
312
  - Role is derived from string `"1"` flags on the user (`user?._isAdmin === "1"`), not booleans.
@@ -262,6 +326,16 @@ Common field attributes: `uuid`, `label`, `valueKey` (path into the data object)
262
326
  slip. Do not "reconcile" the two lists.
263
327
 
264
328
  ## Change history
329
+ - 2026-07-22 — **Correction:** the registry French key for COMPASSCANADA is **`fr-CA`, not `fr`**
330
+ (verified `src/fieldsConfig/index.ts`); documented language persistence (zustand `persist` key
331
+ `"fields-key"`, `setLanguage` stores the option uuid `en`/`fr-CA`) and the `startsWith("fr")` rule —
332
+ a `=== "fr"` comparison caused a real Get Support illustration bug. Documented the "key hard-coded
333
+ JSX through COMPASSCANADA FIELDS with an in-code English fallback so COMPASS/QUAD are unaffected"
334
+ pattern and the 2026-07-22 French-completion keys (`HEADERFIELDS` `adminSubmenu.manageRequests` +
335
+ `alertsSubmenu` group; `ORDERHISTORYFIELDS` orders-for-others headings + `pageSearchInput`; Get
336
+ Support language-switched illustration). Added the `LANGUAGE_SENSITIVE_QUERY_KEYS` mechanism (persona
337
+ query keys now refetch on language switch). Recorded the decision that the "Admin" nav button stays
338
+ "Admin" in French. (tcox)
265
339
  - 2026-06-23 — Initial: documented the two-layer field system (global `clientFields/<TENANT>.json`
266
340
  + the per-page `FIELDS` registry), the `tenant→language→role` resolution in `useAssignClientFields`,
267
341
  the `en`/`fr` vs `ENGLISH`/`FRENCH` distinction, role derivation per tenant, and field JSON shapes
@@ -292,6 +292,17 @@ values, so JSON-only functional fields survive: **Surface owns all display attri
292
292
  visibility, icons, `isRequired`, placeholder, `textAreaHeight`); JSON fills gaps and functional
293
293
  wiring. The per-client `approvalActionFields.json` files are kept on disk as reference (not deleted).
294
294
 
295
+ **Concatenated summary values (`isConcatenated`).** `surfaceBundlesToDecisionFields.ts` now passes an
296
+ element's `config.isConcatenated` through **verbatim** onto the decision-summary row: it exports a
297
+ `DecisionSummaryConcat` type (string shorthand | descriptor `{valueKey, prefix?, suffix?, valueType?,
298
+ layout?: 'inline'|'stacked', label?}`, mirroring blox `BaseDetailField`'s `isConcatenated`) and added an
299
+ optional `isConcatenated?` to `DecisionSummaryField`; `buildSummaryFields` copies `config.isConcatenated`
300
+ straight through. This is purely additive — before it, the adapter dropped the concat config, so the
301
+ Compass / Compass Canada Total `CONFIG` override (stacking `_totalLease` under `_totalPurchase`, seed
302
+ side in [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md)) would have seeded data
303
+ the FE ignored. blox `BaseDetailField` (`renderBaseDetailFieldValue.tsx`) already supports
304
+ `valueKey`+`suffix`+`layout:'stacked'`+`valueType:'currency'`. tsc + eslint clean.
305
+
295
306
  **Shared summary, separate per-action chrome (decision).** The 8 order-summary rows are identical for
296
307
  approve and deny, so they come from **one** shared surface (`sales-order-decision-summary`); the action
297
308
  chrome (title, reason-input required-ness, confirm button, icons, toasts) diverges, so it is **two**
@@ -460,6 +471,14 @@ now carry it (2026-07-21):
460
471
  treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
461
472
 
462
473
  ## Change history
474
+ - 2026-07-21 — Extended `surfaceBundlesToDecisionFields.ts` to emit **`isConcatenated`** on
475
+ decision-summary rows: new exported `DecisionSummaryConcat` type (string shorthand | descriptor
476
+ `{valueKey, prefix?, suffix?, valueType?, layout?, label?}`, mirroring blox `BaseDetailField`); optional
477
+ `isConcatenated?` added to `DecisionSummaryField`; `buildSummaryFields` passes `config.isConcatenated`
478
+ through verbatim. Purely additive (tsc + eslint clean). The adapter previously dropped concat config, so
479
+ the Compass / Compass Canada Total `CONFIG` override (stacks `_totalLease` under `_totalPurchase`)
480
+ would have been ignored FE-side; blox `renderBaseDetailFieldValue.tsx` already renders
481
+ valueKey+suffix+stacked+currency. (apeterson)
463
482
  - 2026-07-21 — Migrated the **SalesOrders approve/deny decision modal** display config onto Surface via
464
483
  an **overlay** adapter seam. New `surfaceBundlesToDecisionFields.ts` adapts the 3 resolved bundles
465
484
  (`DECISION_SURFACE_SLUGS`) into `{orderContentDetails, approve, deny}`, finding elements by
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
17
17
 
18
18
  ## 2.0 framework
19
19
 
20
- - **_underscore** (_Underscore) _(framework core)_ — 36 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
+ - **_underscore** (_Underscore) _(framework core)_ — 37 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
21
21
  - **worker2** (Worker) — 31 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
22
  - **api2** (API) — 13 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
23
23
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -14,7 +14,7 @@ project: _Underscore
14
14
  client: compass-canada
15
15
  type: profile
16
16
  status: active
17
- updated: 2026-07-16
17
+ updated: 2026-07-22
18
18
  owners: [jcardinal, bala, tcox, apeterson]
19
19
  files: []
20
20
  related:
@@ -70,6 +70,15 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
70
70
  (16 rows seeded). French was extracted from the old bilingual `"English/French"` `Assortments.name`
71
71
  values, which were then cleaned to English-only. See
72
72
  [Assortment Name Translation](../../2.0/apps/_underscore/features/assortment-name-translation.md).
73
+ - **Persona names (fr-CA) — DRAFTED / DEFERRED, not yet run.** A `PersonaTranslations` sidecar copying
74
+ the assortment pattern (Core record 346, fr-CA seed for 33 personas) is fully drafted in working
75
+ trees and `sql-reviewer`-approved but **not committed or run** — ids and 3 name-matched personas need
76
+ live-DB verification first (RDS was VPC-unreachable). The storefront side (persona query keys made
77
+ language-sensitive) already shipped. See
78
+ [Persona Name Translation](../../2.0/apps/_underscore/features/persona-name-translation.md).
79
+ - **Storefront French localization** (labels, submenus, orders-for-others, Get Support illustration) is
80
+ driven by the COMPASSCANADA FIELDS JSON; the registry language key is **`fr-CA`** (not `fr`). See
81
+ [Client Fields](../../2.0/apps/toga2-commerce/features/client-fields.md).
73
82
  - The 2026-06-08 ASN → ItemFulfillment work was for **Compass USA**, not Compass Canada.
74
83
  - **Order status is shipped-only**, same as Compass USA (shared `_Model_Compass_SalesOrder`).
75
84
  Compass Canada's own IF lifecycle stages (picked/packed/shipped) + shipped backfill are seeded by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.413",
3
+ "version": "1.0.415",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",