# Sources and recipes

Where every column of the map comes from, how to trace it, and the traps that produce a confidently wrong row. The whole point of the skill is a cross-reference: each column is checked against at least one other source, never skimmed from one.

## Where each input lives

| Input | Source | Access |
|---|---|---|
| Figma structure (components, nodes, screenshot) | In-repo design export | Local files |
| CMS annotations and overlay geometry | Figma REST API (keychain `FIGMA_ACCESS_TOKEN`); Figma MCP in Dev Mode as the rate-limit-free alternative; the export as offline fallback | Network |
| Screenshot | `screens/<frame>/screenshot.png` in the design export | Local |
| Specs repo files | `gh api repos/<owner>/<repo>/contents/<path>` with `Accept: application/vnd.github.raw`; no checkout needed | Network |
| Redesign app and resources repo | Local checkouts | Local |
| Legacy values | `resources/Localization/Legacy/<lang>.json`, a flat snapshot that is refreshed from the label service | Local, network on refresh |

The Android app's committed `strings.xml` is only a key catalog: its values default to the key itself. Real legacy values live in the iOS bundled `language.plist` and, authoritatively, in the label service.

## New keys: the three-way union

A screen's keys come from three places, and each one misses something the others see:

1. **Design export**: read `screens/<frame>/components-used.md` of each state frame the screen has (default, error, loading, filled...).
2. **Component registry**: each used component's `<node>.json -> localizationKeys`. These are keys the component renders internally, and they are often absent from the screen code.
3. **Screen code**: on iOS, each call site of `<KeyType>.<Namespace>.<leaf>` counts as one element, where `<KeyType>` is the type the project generates its keys into. Pass it to `scan-screen-keys.py` and `verify-map.py` with `--key-type <Name>` (repeatable, or a comma list), or set `"keyType"` in the mapping for `verify-map.py`; the default is `L10n`.

Take the union and tag every key: screen-specific, component-owned, or shared (a reusable component or a shared primitive).

Registry caveat: a shared component such as `ScreenHeader` or `ActionFooter` may list keys that this screen never shows, since the screen supplies its own title. Check the code before a registry key goes into the map.

### Keys the union cannot see

Error, validation and alert strings, and keys assembled at runtime, rarely appear in the design export or the registry. Run the scanner on the implemented screen:

```bash
python3 scripts/scan-screen-keys.py --out scan.json --screen-path <screen-dir> --key-type <KeyType>
```

Every `error` and `dynamic` hit becomes a row. Compare the `static` hits with the union; whatever remains is a key the code wires but the map lacks. Skipping this step drops those strings silently.

## New values

Values are authored per key in `Suggested/<Key>.json`, one entry per language the project ships, and they are shared by all platforms: one value per key, only the key usage differs per platform.

```bash
python3 scripts/resolve-new-values.py --keys "<k1>,<k2>" --resources-root <res> --langs all \
  --catalog <path/to/shipped/Localizable.xcstrings>
```

Always pass `--catalog`. A missing Suggested file means "not in this snapshot", not "unauthored": the snapshot can lag the shipped catalog. With the catalog the value still resolves and the script names the stale snapshot so it can be refreshed with `snapshot-resources.sh`. Only a key absent from both the snapshot and the catalog is truly unauthored. A catalog entry that is empty, or has no value in any requested language, counts as absent: the lookup then tries the normalised key form and otherwise lists the key as unresolved. Never open an authoring request for a key the catalog resolved.

## CMS copy (Figma annotations)

The content team writes final copy as Dev Mode annotations in the category "Final UX Writing", usually as `TR: ...` and `EN: ...` lines. The text drawn in the design is often placeholder copy; the annotation is the real value.

```bash
python3 scripts/fetch-annotations.py --out _annotations.json --mapping <map>.json
python3 scripts/fetch-annotations.py --out _annotations.json --from-mcp <raw-mcp-dump>.json
python3 scripts/fetch-annotations.py --out _annotations.json --local <design-export>/screens/<frame>
```

The default source is REST; when it fails and `--local` is given, the export's `tree.json` is read instead. `--from-mcp` is never tried automatically: after a rate limit (HTTP 429), dump the annotations through Figma MCP and rerun with that file. Node-ids in the URL form `1024-4150` are read as `1024:4150`; an annotation without a node-id is kept as its own entry, never merged with another.

Pair every annotation with its row through `nodeId`: the id is saved in the row as `cmsNodeId`, and the copy goes into `cms: {tr, en}`. An element without an annotation keeps a blank CMS cell. The script lists nodes that have TR but no EN; send those back to the content team.

## Legacy keys: trace, do not skim

### iOS

The spec names the entry point: `specs/<area>/<screen>-spec.md` carries a blockquote `> Legacy reference (iOS):` naming the class. Follow the chain from there:

view controller, then view model, then data source, then the cell presentation models, where the `"Key".localized` calls usually are.

Form-field labels are normally defaulted inside the cell presentation models (often shared input-cell models), not in the view model. Do not stop at the view model.

### Android

Labels come from the layout XML as `@string/Key`; strings set at runtime come from the fragment as `R.string.<name>`. For form-field labels the layout is the most reliable place to read them.

### Web

Call sites are `t('key')` or `i18n.t('key')` (react-i18next, next-intl) and `$t('key')` (vue-i18n), resolved against `locales/<lang>.json`. A template literal or a variable argument is a dynamic key. A namespace from `useTranslation('<ns>')` is prepended to the key, so `t('email_label')` under `useTranslation('signup')` is `signup.email_label`.

### Cross-platform drift

The same element can carry different keys per platform (`AgentaUserAlreadyAdded` on one, `AgencyUserAlreadyAdded` on the other). That is a `review`. Keep every platform column that has a key and render a dash where a platform has none.

## Legacy values

```bash
python3 scripts/resolve-legacy-values.py --keys "Continue,EmailAddress" \
  --snapshot-root resources/Localization/Legacy --langs all --prefix App-
```

- Reading the snapshot needs no network. `fetch-legacy-labels.py` refreshes it and is the only step that goes online.
- `--live` checks against the service directly; if the live call fails, the script falls back to the snapshot, then to the plists.
- With none of `--snapshot-root`, `--plist-root` or `--endpoint`, the script stops with a message naming them.
- `--plist-root` reads the bundled `<lang>.lproj/language.plist` as a last resort. It is usually a stale subset, so a hit from it deserves a note.
- **Backend prefix.** When the backend stores `App-Continue` but the app asks for `Continue`, pass `--prefix App-`, keep the mapping keys bare, and set the mapping's `legacyKeyPrefix` so the table shows the full stored key.

## Overlay geometry

The overlay does not depend on annotations. With zero annotations every keyed element still gets a gray "awaiting copy" card. The number on a card and on its dot is the index of the matched row in `mapping.rows`, counted from 1, so it agrees with the table rather than with the order on screen.

**REST (preferred).** Needs `figmaFileKey` and the real frame node. Take the frame node-id from the design export's `tree.json` root `nodeId`: folder-name suffixes and ids copied from older maps are often wrong. Check that the node text belongs to this screen; background nodes that match no row are dropped automatically.

**`--spec` (screenshot-anchored).** Use it when there is no token or when REST geometry is noisy. Measure the box of every visible element on the clean screenshot, give the rows synthetic `cmsNodeId`s (`n1`, `n2`, ...), and write:

```json
{"fileKey": "...", "pages": [{"nodeId": "1024:4096", "image": "screenshot.png",
  "frame": {"w": 375, "h": 812},
  "nodes": [{"id": "n1", "x": 16, "y": 64, "w": 220, "h": 28, "side": "left"}]}]}
```

Every page needs `frame` with numeric `w` and `h`; a page without it stops the script with an error naming the page. `render-overlay.py` draws only the first page and warns when there are more; `render-key-shots.py` reads every page. `image` is either a data URL or a PNG path, resolved from the folder of the spec file. `side` is `left` (default) or `right`: it puts the card on the side where its target sits, which two-column layouts need. Each side is numbered and stacked independently, and right-side connectors are mirrored.

The export's `tree.json` stops at component instances and carries no geometry per label, so take only the frame node-id from it.

Component-owned keys (list rows, promotion grids, summary cards) are marked on the overlay too, flagged `(owned by <Component>)`, with their keys taken from the registry `localizationKeys`.

**Multi-state screens.** Each state gets an overlay of its own, built from its own `--spec`, screenshot and `cmsNodeId`s; a row may keep the same `cmsNodeId` in several states. Stack the results vertically in a single PNG, padded to the widest one with a `#1e1e1e` gutter between them, point the mapping's `overlay` at that PNG, and keep the per-state images next to it.

**Isolating a component variant.** Once the library is re-versioned, node-ids exported from it stop resolving: `/components` may return nothing and `/images` returns `null`. Walk the live screen frame instead (`GET /v1/files/<key>/nodes?ids=<frame>&depth=6`), find the `INSTANCE` whose `name` matches, render that id, and take label boxes from its text nodes' `absoluteBoundingBox` multiplied by the render `scale`. Without a token, or when the render fails, cut the variant from a screenshot of one of the states.

## Keyshots

For each matched row, `render-key-shots.py` saves a crop with the element outlined in red as `keyshot__<NN>__<key>.png` inside `keyshots/`, where NN is the row number in the table, and it writes `<slug>.keyshots.manifest.json`. A row with no match gets a dash, never a guessed crop. Set the mapping's `keyshots` to the manifest file name.

Geometry reuses the overlay inputs: REST (visible text only, filtered for ancestor visibility and cumulative opacity) or the same `--spec` files, one per state, repeatable; `image` may be a path such as the committed `screenshot.png`.

## Pitfalls

- **Truncated trees.** On a large specs repo the recursive Git tree is cut off at roughly 43k paths. Search with `gh search code` or fetch through `contents/` calls rather than `git/trees/HEAD?recursive=1`.
- **Namespaced vs flat keys.** `SignUpAccountDetails.ContinueButton` against legacy `Continue`: reuse is about the value, not the key string.
- **Hidden form labels.** They sit in shared input-cell models; read them from the Android layout.
- **Blank is fine.** Empty Old and CMS columns are expected states, not errors.
- **Scanner skipped.** Error, validation, alert and dynamic keys never show up in the static cross-reference.
- **Frame origin drift.** A rendered frame PNG may not share the frame's `absoluteBoundingBox` origin. Subtracting the frame x/y from text-node x/y has shifted boxes by tens of pixels, a full row off. Check one crop by eye, and prefer boxes read off the actual screenshot to coordinates mixed from two sources.
- **Stale snapshot.** Missing from Suggested is not unauthored; pass `--catalog` and refresh the snapshot instead of filing requests.
- **Value anchoring.** Never anchor a keyshot by value across a whole file: short labels recur on unrelated screens and give a confidently wrong crop, which is worse than a dash. Recover a row only through its own `cmsNodeId`, and leave `characters` out of nodes handed to `render-key-shots.py --spec`, because text present turns value matching back on and can bind an unrelated row.
- **Confluence auth.** Server / Data Center takes a Bearer PAT (keychain `CONFLUENCE_API_TOKEN`), not the Cloud `user:token` form. Pages are looked up with the `content?title=` query, which reads the database; CQL is avoided because its index lags and leads to duplicate pages.
- **Secrets.** Never commit a label-service token, Confluence PAT or Figma token into a mapping, a taxonomy file or the docs. No script takes a token on the command line: `--token` exits 2, because other processes can read argv. Use the environment variable or the keychain item each script names.
- **TLS proxies.** Publishing runs through `curl` so a corporate proxy's trust store is honoured (Python `urllib` fails verification there). The token goes in a `chmod 600` config passed with `-K`, never on argv. For the Figma scripts behind such a proxy, export the system roots with `security find-certificate -a -p > roots.pem` and pass `--ca-file roots.pem`.
