toga-ai 1.0.177 → 1.0.178
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.
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
|
|
6
|
+
| [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple languages without forking the schema or breaking English consume | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql |
|
|
6
7
|
| [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
|
|
7
8
|
| [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
|
|
8
9
|
| [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | |
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Language Translation Layer (audience.language + sidecar tables)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: api2
|
|
5
|
+
project: API
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-06-23
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- api2/Component/Api/V2/V2.php
|
|
13
|
+
- _underscore/Model/Core/Setting.php
|
|
14
|
+
- _underscore/Model/Core/RecordField.php
|
|
15
|
+
- _underscore/Model/Core/DefaultGlobalSetting.php
|
|
16
|
+
- _underscore/Model/Client/ItemTranslation.php
|
|
17
|
+
- dbchanges2/Client/2026-06-23a - ItemTranslations.sql
|
|
18
|
+
- dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql
|
|
19
|
+
- dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql
|
|
20
|
+
- dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql
|
|
21
|
+
related:
|
|
22
|
+
- ./acl-permission-chain.md
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Summary
|
|
26
|
+
|
|
27
|
+
Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple
|
|
28
|
+
languages without forking the schema or breaking English consumers. English stays in the source
|
|
29
|
+
table; per-language overlays live in per-table **sidecar** tables (`ItemTranslations` first).
|
|
30
|
+
The caller's language is resolved once at authentication, surfaced on every response's
|
|
31
|
+
`audience.language`, and embedded in the JWT; reads/writes are redirected to the sidecar in the
|
|
32
|
+
**API layer** (`V2.php`) — the `_Model` ORM is untouched, so workers/crons/cXML keep returning
|
|
33
|
+
base English. Missing translations fall back to English with a warning in `messages[]` (still 200).
|
|
34
|
+
|
|
35
|
+
## Key files / entry points
|
|
36
|
+
|
|
37
|
+
- `_underscore/Model/Core/Setting.php` — `SLUG_LANGUAGE` const + `valueIsSet()` helper (the cascade
|
|
38
|
+
resolver itself lives in V2.php, since it is a language/API concern).
|
|
39
|
+
- `api2/Component/Api/V2/V2.php`:
|
|
40
|
+
- `resolveSettingValue($slug, $context)` — the 8-layer Settings-cascade resolver (private).
|
|
41
|
+
- `resolveValidLanguageCode()` — validates the resolved code against `Languages.code`, default `en`.
|
|
42
|
+
- `getTranslatedFieldValue()` / `loadTranslationSidecar()` / `shouldTranslate()` / `addTranslationFallbackWarning()` — read path.
|
|
43
|
+
- `extractTranslationWrites()` / `saveTranslationWrites()` — write path.
|
|
44
|
+
- `lookupTranslatableFieldByRecordFieldId` built in the RecordFields load (~line 200).
|
|
45
|
+
- `_underscore/Model/Client/ItemTranslation.php` — the first sidecar model.
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
**Language resolution (auth time, user auth only).** `resolveSettingValue('language', …)` walks the
|
|
50
|
+
Settings Matrix in order — DefaultGlobal, DefaultApp, ClientGlobal, ClientApp, PersonaGlobal,
|
|
51
|
+
PersonaApp, UserGlobal, UserApp — each overriding the previous when set. A Client/Persona layer with
|
|
52
|
+
`isOverridable = 0` **locks** the value against all later layers. For the persona layers, the **first
|
|
53
|
+
persona** (in the user's persona order) that sets it wins. The resolved code is validated against
|
|
54
|
+
`Languages.code` (fallback `en`), embedded in the JWT `id.language` claim, and echoed on every
|
|
55
|
+
response as `audience.language`. API-credential auth gets no language. Token refresh copies the claim,
|
|
56
|
+
so a language change requires re-authentication.
|
|
57
|
+
|
|
58
|
+
**Which fields are translatable (metadata-driven).** `Core.RecordFields.translationRecordFieldId`
|
|
59
|
+
(new column, positioned after `recordId`) points a source field's RecordField at the sidecar field's
|
|
60
|
+
RecordField. `buildLookups`/RecordFields-load resolves this into
|
|
61
|
+
`lookupTranslatableFieldByRecordFieldId[sourceRecordFieldId] => {sidecarRecordId, sidecarField}`.
|
|
62
|
+
|
|
63
|
+
**Read path (PHP `_Model::load()`, NOT a SQL JOIN — deliberate choice).** At every response
|
|
64
|
+
serialization site (top-level full-model, custom-fields, FK child, both inherent-child paths)
|
|
65
|
+
`getTranslatedFieldValue($record, $field, $sourceModel, $defaultValue)` is called. When a non-base
|
|
66
|
+
language is active and the field is translatable, it loads the sidecar row for that source row +
|
|
67
|
+
`languageId` (cached per row so multiple fields = one load) and returns the sidecar value if non-null;
|
|
68
|
+
otherwise it returns the English default and queues a deduped `W*` warning.
|
|
69
|
+
|
|
70
|
+
**Write path.** On create and update, `extractTranslationWrites()` pulls translatable fields out of
|
|
71
|
+
the write set for a non-base language (so the English source is never overwritten), and after the
|
|
72
|
+
source row saves, `saveTranslationWrites()` upserts them into the sidecar for the current language.
|
|
73
|
+
Nested-child translatable writes are NOT auto-redirected — use the dedicated `/v2/item-translations`
|
|
74
|
+
endpoint for those.
|
|
75
|
+
|
|
76
|
+
## Data model
|
|
77
|
+
|
|
78
|
+
- `Client.ItemTranslations` — `id, uuid, dtCreated, dtUpdated, itemId (FK Items, RESTRICT),
|
|
79
|
+
languageId (FK Languages, RESTRICT), title, description, longDescription`; `UNIQUE(itemId, languageId)`.
|
|
80
|
+
English stays in `Items`; sidecar holds only non-English overlays (null → English fallback).
|
|
81
|
+
- `Core.RecordFields.translationRecordFieldId` — new nullable self-FK (after `recordId`).
|
|
82
|
+
- `Core.Records` 331 = `item-translations` (`aclDatabase='CLIENT'`); `Core.RecordFields` 2233–2239
|
|
83
|
+
(its fields), 2240 (the self-describing `translationRecordFieldId` field).
|
|
84
|
+
- `item-translations` ACL chain lives in each client DB targeting the Base role — see
|
|
85
|
+
[acl-permission-chain.md](./acl-permission-chain.md).
|
|
86
|
+
|
|
87
|
+
## Client variations
|
|
88
|
+
|
|
89
|
+
None — uniform across all clients. The sidecar table + ACL ship via `dbchanges2/Client/` (all client DBs).
|
|
90
|
+
|
|
91
|
+
## Gotchas / known issues
|
|
92
|
+
|
|
93
|
+
- The resolver/read/write deliberately use `_Model::load()` per row, not a SQL JOIN/COALESCE — less
|
|
94
|
+
SQL-efficient but the team's chosen approach; the sidecar is cached per row to avoid N-per-field loads.
|
|
95
|
+
- `audience.language` reflects the *requested* language even if a given field lacks a translation; the
|
|
96
|
+
per-field fallback warning signals the English fallback.
|
|
97
|
+
- The base language is `en`; when the resolved language is `en` (or API auth) all translation logic is
|
|
98
|
+
skipped and responses are byte-identical to pre-feature.
|
|
99
|
+
- Migrations not yet executed at time of writing; needs live verification.
|
|
100
|
+
|
|
101
|
+
## Change history
|
|
102
|
+
|
|
103
|
+
- 2026-06-23 — Initial build: audience.language + JWT embedding, ItemTranslations sidecar + metadata +
|
|
104
|
+
full ACL chain, and translation-aware read/write at the API layer. Also fixed a latent autoload bug
|
|
105
|
+
by renaming `DefaultFlobalSetting.php` → `DefaultGlobalSetting.php`. (jcardinal)
|
|
106
|
+
|
|
107
|
+
## Related docs
|
|
108
|
+
|
|
109
|
+
- [ACL Permission Chain](./acl-permission-chain.md)
|
|
110
|
+
- [api2 Architecture](../architecture.md)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -17,7 +17,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
17
17
|
|
|
18
18
|
- **_underscore** (_Underscore) _(framework core)_ — 11 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
19
19
|
- **worker2** (Worker) — 12 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
20
|
-
- **api2** (API) —
|
|
20
|
+
- **api2** (API) — 6 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
21
21
|
- **dbchanges2** (Database Changes) _(framework core)_ — 2 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
22
22
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
23
23
|
- **saml** (SAML SSO Gateway) — 2 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
package/package.json
CHANGED