# Sources & retrieval recipes (the rigorous method)

> Bundled detail for [../SKILL.md](../SKILL.md). The data model and the exact, copy-paste recipe per
> source. **Retrieval is a cross-reference, not a skim**  -  that is the whole point of this skill. Every
> recipe reads files or uses `gh`; no other skill required.

## The data model  -  two sides, sources per side

| | UI element → key | key → translation value |
|---|---|---|
| **New (redesign)** | `design-export` components-used **×** component registry `localizationKeys` **×** the screen's code (what it wires). Cross-reference all three. | `<resources-repo>` → `Sources/Suggested/<Key>.json` (8 langs)  -  `scripts/resolve-new-values.py` |
| **Legacy** | the screen **traced** through its code: iOS VC→VM→DataSource→**cell presentation models**; Android layout `@string/` + fragment `R.string.`; web component → `t('key')`/`$t('key')` call site | iOS shipped label map `<lang>.lproj/language.plist` (`Mobile-<Key>`, 8 langs)  -  `scripts/resolve-legacy-values.py`; gaps → labels-API snapshot; web → its `locales/<lang>.json` (or equivalent i18n catalog), same snapshot script |

Read locally: the redesign app (code) + `<resources-repo>` (new values + registry).
Over `gh` (no checkout): `<specs-repo>`  -  holds `design-export/`, the legacy iOS/Android source, the
shipped `language.plist`, and `specs/`. New translation **values are shared across platforms** (one
Suggested value per key); only key *usage* is per-platform.

**A third value source  -  the CMS (content team) copy.** Beyond `new` (design/resources) and `legacy`, the
content team writes the **actual final copy** as Figma **Dev Mode annotations** ("Final UX Writing", `TR:/EN:`).
That copy is fetched live (`fetch-annotations.py`) and shown additively as the **CMS** columns  -  see *CMS
values* below. And because the static element→key cross-reference misses runtime strings, a **scan of the
implemented screen** (`scan-screen-keys.py`) recovers error / dynamic / registry-missed keys  -  see *Catching
the keys the cross-reference misses* below.

## New side  -  element → key (CROSS-REFERENCE, not code-only)

A component's registry `localizationKeys` are what it renders **internally**; the screen's code shows
what it **wires**; neither alone is complete. Do all three:

1. **Which components + nodes the screen comprises**  -  `design-export` (single-repo, the snapshot model):
   ```bash
   gh api "repos/<org>/<specs-repo>/contents/design-export/<project>/screens/<screen-slug>/components-used.md" -H "Accept: application/vnd.github.raw"
   ```
   Read **every state frame** of the screen (empty / 1-item / filled)  -  components only present in the
   filled state (e.g. a passenger list) still belong to the screen. The folder also has `screenshot.png`,
   `tree.json` (node ids), `tokens.json`. *(Live alternative if a frame isn't exported: Figma REST via a
   host Figma tool  -  REST works with a token; MCP/live needs Figma Desktop in Dev Mode. The export is
   preferred  -  single-repo.)*
2. **Each component's owned keys**  -  registry, local & instant:
   ```bash
   python3 -c "import json;d=json.load(open('Resources/Figma/Components/<NODE-DASH>.json'));print([k['key'] for k in d.get('localizationKeys',[])])"
   ```
   These include keys the component renders that **never appear in the screen code** (e.g.
   `PassengerListGroup.Title`, `PassengerListRow.Delete`). Beware the inverse: a shared
   component (e.g. `HeaderMain`, `BottomActions`) owns many keys the screen does **not** use because the
   screen passes its own title in  -  confirm against the code.
3. **What the screen wires**  -  read the scene/screen file: each `LocalizationStringKey.<Ns>.<leaf>` call
   site is one element; the call site is the label. Screen-specific keys (`SignUpAccountDetails.*`) and
   reused keys (`ModalsCustoms.Ok`) live here.

Union the three; tag each key **screen-specific / reusable-component / shared-primitive**. Component-only
keys: map once where that component is mapped  -  flag, don't duplicate per screen.

**New values:** `python3 scripts/resolve-new-values.py --resources-root <resources-repo> --keys "..." --langs all`.

## Legacy side  -  element → key (TRACE the screen, don't skim the map)

1. Start from `specs/<area>/<screen>-spec.md`  -  its `> Legacy reference (iOS):` blockquote names the entry
   class (VC/VM).
2. **iOS  -  follow the chain.** Fetch the VM raw; it builds sections/cells. Form-field labels are **not** in
   the VM  -  they default inside the **cell presentation models** it instantiates. Follow them:
   ```bash
   gh api ".../contents/<path>/<Screen>ViewModel.swift" -H "Accept: application/vnd.github.raw" > /tmp/vm.swift
   grep -nE 'PresentationModel|\.localized' /tmp/vm.swift          # find the cell classes it creates
   # then fetch each cell class and grep its keys:
   gh api ".../Forms/TextInputCell/PresentationModels/<Field>TextInputCellPresentationModel.swift" -H "Accept: application/vnd.github.raw" | grep -oE '"[^"]+"\.localized'
   ```
   This is how `NewSurnameReq` / `PnrNumber` / `ReservationEticketPlaceHolder` surface  -  a VM-only grep
   misses them. Keys are the string literal before `.localized`.
3. **Android.** Layout XML carries labels as `@string/Key`; fragment carries runtime strings as `R.string.`:
   ```bash
   gh api ".../res/layout/<screen>.xml" -H "Accept: application/vnd.github.raw" | grep -oE '@string/[A-Za-z0-9_]+' | sort -u
   ```
4. **Web (when the legacy app is a web frontend).** The component wires the key as an i18n call, not a
   string literal  -  grep the component tree for the call site, not the copy:
   ```bash
   gh api ".../src/screens/<Screen>/index.tsx" -H "Accept: application/vnd.github.raw" \
     | grep -oE "(\\\$?t\(['\"]|useTranslation\(['\"])[A-Za-z0-9_.]+" | sort -u
   ```
   Covers `t('key')` / `i18n.t('key')` (react-i18next, next-intl) and `$t('key')` (vue-i18n). A template
   literal or variable argument (`` t(`errors.${field}`) ``) is a **dynamic** key exactly like iOS
   `String(format:)`  -  record the pattern, not a resolvable literal, same rule `scan-screen-keys.py`
   applies. Namespace prefixes (`t('signup.email_label')` inside `useTranslation('signup')`) resolve to
   `signup.email_label`  -  read the hook/provider call, don't assume the literal alone is the full key.

iOS, Android and web usually share the flat key for shared elements; when any platform differs it is
drift worth a **review** (e.g. `AgentaUserAlreadyAdded` iOS typo vs `AgencyUserAlreadyAdded` Android).
Record every column present; `  -  ` where a platform has none.

## Legacy translation values  -  the in-repo legacy-label snapshot (default), refreshed from the live service

Values come from your legacy backend’s label service (the same map the legacy apps render)  -  but **snapshotted
in-repo** so the mapper reads them offline, no network round-trip per run (single-repo, like design-export):
```bash
python3 scripts/resolve-legacy-values.py --langs all \
  --snapshot-root <specs-repo>/resources/Localization/Legacy \
  --keys "Continue,EmailAddress,PasswordRule,EmailAlreadyExists"
```
The snapshot is `resources/Localization/Legacy/<lang>.json`  -  a flat `{key: value}` map per language.
If your backend namespaces keys under a prefix the app strips (a stored `Mobile-Continue` for the app's
`Continue`), pass `--prefix Mobile-` and keep the mapping's keys bare; set the mapping's `legacyKeyPrefix`
so the rendered table still shows the content team the full stored key.

**Refresh** the snapshot (the only network step) with `fetch-legacy-labels.py`. Legacy translations are
served by *your* backend, so no endpoint is built in  -  you describe it once:
```bash
python3 scripts/fetch-legacy-labels.py --out <specs-repo>/resources/Localization/Legacy --langs all \
  --endpoint 'https://labels.example.internal/{env}/labels/{lang}' --env dev \
  --headers-file .secrets/label-headers.json \
  --fail-status '9999=gateway blocked the request' --fail-status '50=client version not accepted'
```
- Keep tokens in `--headers-file` (a JSON object), never in `--header`  -  argv lands in shell history and CI logs.
- The response shape is discovered, not assumed: the largest flat `{string: string}` map anywhere in the JSON
  wins, so an enveloped payload works without configuration.
- `--fail-status CODE=message` turns your backend's in-band error codes into a clear abort instead of an
  empty snapshot. Find these once and record them in your project's own notes.

`resolve-legacy-values.py --live` (same `--endpoint` / `--headers-file`) bypasses the snapshot to read the
service directly, to verify or refresh. `--plist-root <dir>` is a last-resort offline fallback for a legacy
iOS app's bundled `<lang>.lproj/language.plist`  -  typically a stale subset, so a hit there deserves a note.
Never invent a legacy value  -  blank + a note beats a guess.

## CMS values  -  the content team's actual copy (Figma Dev Mode annotations)

The content team annotates each text node with the **final** bilingual copy as a Dev Mode annotation
(category *Final UX Writing*), `TR: ...` / `EN: ...`. The design text is usually a placeholder ("Giriniz",
"Lorem ipsum")  -  the **annotation**, not the design text, is their real value. Figma is edited continuously,
so fetch it **live**, with fallbacks (priority order):

```bash
# 1) REST (default)  -  needs a Figma token (keychain FIGMA_ACCESS_TOKEN, env, or --token):
python3 scripts/fetch-annotations.py --mapping <map>.json --out _annotations.json
#    (or --file <fileKey> --nodes "1024:4096,1024:4112")
# 2) MCP (no rate limit; the host's Figma plugin dumped the nodes' annotations):
python3 scripts/fetch-annotations.py --from-mcp _annotations.raw.json --out _annotations.json
# 3) local fallback (offline; only if design-export tree.json carries annotations):
python3 scripts/fetch-annotations.py --local design-export/<project>/screens/<slug> --out _annotations.json
```
It calls Figma `GET /v1/files/<key>/nodes?ids=...` (header `X-Figma-Token`), walks `node.annotations[].label`,
parses `TR:/EN:` (the same `split_lang` rules the web skill uses), and emits
`{nodeId, designText, tr, en, raw, mode}` per annotated node. Map each annotation onto its row by
**`nodeId`** (store it as the row's `cmsNodeId`) → fill the row's `cms:{tr,en}`. A `429` aborts with a "use
`--from-mcp`" hint. **Never invent a CMS value**  -  leave `cms` absent if there's no annotation. A CMS value
that differs from `new` is fine (the renderer marks it ⚠); a CMS-only `TR` with no `EN` is flagged back to
the content team.

## Catching the keys the cross-reference misses  -  scan the implemented screen

The static element→key cross-reference (registry × design-export × code) is biased toward **statically-wired
literal** keys. It is blind to **error/validation/alert** copy (lives in validators, error mappers, alert
builders, VM error branches  -  not the VC→VM→cell chain) and to **dynamic/interpolated** keys
(`String(format:)`, variable `.localized` receivers, `LocalizationStringKey(<expr>)`, Android
`getString(<var>)`). Recover them by scanning the implemented screen directly:

```bash
python3 scripts/scan-screen-keys.py --screen-path <impl-screen-dir>     # [--category error|dynamic|static]
```
It greps `.swift`/`.kt`/`.xml`/`.ts`/`.tsx`/`.js`/`.jsx`/`.vue` and classifies each hit `static | error |
dynamic` with file:line. **Fold the
`error` and `dynamic` hits into the mapping** (each still gets the normal new/legacy/CMS resolution); diff the
`static` hits against the registry union to catch keys wired in code but absent from the registry. Dynamic
keys that can't be statically resolved are mapped with a note (`verdict: review` / "dynamic  -  enumerate at
runtime"), never silently dropped.

## Pitfalls
- **Code-only / map-skim misses keys.** New side needs registry × design-export × code; legacy side needs
  the cell-chain trace. This is the difference between right and wrong keys.
- **Static cross-reference misses runtime strings.** Error/validation/alert copy and dynamic/interpolated
  keys are invisible to the literal greps  -  run `scan-screen-keys.py` over the implemented screen and fold
  in the `error`/`dynamic` hits, or they silently vanish from the map.
- The recursive Git tree of `<specs-repo>` truncates (~43k paths)  -  use `gh search code` / direct
  `contents/` calls, not `git/trees/HEAD?recursive=1`.
- New namespace ≠ legacy flat key  -  "reuse" means reuse the *value* (or alias the key), not string equality.
- Legacy values default to the key in the committed Android `strings.xml`  -  that file is a key *catalog*,
  not values; values live in the iOS `language.plist` (and the service).
