# Mapping format and outputs

The mapping JSON is the single source every renderer reads. The scripts add nothing of their own: what is not in the mapping does not appear in the table, the spreadsheet or the page.

## Mapping schema

Top level:

| Key | Required | Meaning |
|---|---|---|
| `screen` | yes | Screen name; the file slug is derived from it |
| `platforms` | no | e.g. `["ios", "android", "web"]` |
| `figmaNodes` | no | Array of frame node-ids |
| `figmaFileKey` | no | Enables the live annotation fetch and the REST overlay |
| `screenshot` | no | Screen image, relative to the mapping |
| `overlay` | no | Overlay PNG name |
| `keyshots` | no | Keyshots manifest name |
| `legacyKeyPrefix` | no | Prepended to legacy keys for display only |
| `keyType` | no | The project's key accessor type, a string or a list of strings (for example `"Strings"` or `["Strings", "A11y"]`); `verify-map.py` uses it when `--key-type` is not given, else `L10n`. Any other JSON type stops the gate with exit 2 |
| `rows` | yes | One object per UI element |

Row:

| Key | Meaning |
|---|---|
| `element` | Human label for the UI element, in the `--ui-lang` language |
| `newKey` | Redesign key; may carry `(owned by <Component>)` |
| `new` | Eight-language map: `en`, `tr`, `ar`, `de`, `es`, `fr`, `it`, `ru` |
| `legacyKeyIOS`, `legacyKeyAndroid`, `legacyKeyWeb` | Legacy key per platform. Empty or omitted renders a dash. Web only when the legacy app has a web frontend. A value in parentheses such as `(nav title)` is a marker and is shown as is |
| `legacy` | Eight-language map; `""` where the value is only served by the backend and was not fetched |
| `cms` | Optional; usually `tr` and `en` from the Figma annotation |
| `cmsNodeId` | Optional; the annotated (or synthetic spec) node-id |
| `cmsSource` | Optional: `figma-rest`, `figma-mcp`, `local` or `none` |
| `verdict` | `reuse`, `review` or `new` |
| `note` | Reasoning, edge-case flags, `(owned by ...)`, "dynamic - enumerate at runtime" |
| `propertyGroup`, `propertyModule` | Optional spreadsheet-only overrides |

Keep `new` and `legacy` keyed by every language the project ships. The summary table shows old and new `en`/`tr` plus CMS `tr`/`en`; the details section shows every language of the mapping (or the `--langs` list) plus every CMS language present.

### The example

[`../example-mapping.json`](../example-mapping.json) maps a neutral "Sign Up - Account Details" screen in nine rows, with `legacyKeyPrefix: "App-"`:

| # | Case | Verdict |
|---|---|---|
| 1 | Nav title with no iOS legacy key (`(nav title)` marker) | review |
| 2 | One shared key, identical on all three platforms | reuse |
| 3 | Placeholder with no legacy counterpart | new |
| 4 | Component-owned `PasswordField.StrengthHint (owned by PasswordField)` with cross-platform key drift | review |
| 5 | Duplicate-email error found only by the scanner; the Android key has a typo | review |
| 6 | CTA whose web key is namespaced (`signup.continue_button`) | review |
| 7 | `tr` equal to `en` in the redesign source | reuse, flagged untranslated |
| 8 | CMS-only `tr` that differs from `new` | new |
| 9 | Dynamic `Error.<field>Required` | review, "enumerate at runtime" |

## Rendered artifacts

`build-artifact.py <map>.json --out <dir>` always writes three files, and more on request:

| File | Contents |
|---|---|
| `<slug>.md` | Title, intro, platforms and Figma nodes, screenshot, optional key-map image, summary with counts, drift note, pipe table (pipes escaped, newlines flattened, keyshot as a relative image), per-row detail tables |
| `<slug>.confluence.xml` | Storage format, the value of `body.storage.value`: header paragraph, an `info` macro with the legend (status macros), counts, CMS line and drift note; a Screen section (`<ac:image ac:height="500">`); a key-map section (`<ac:image ac:width="900">`); the summary table with keyshots (`<ac:image ac:width="240">`) referenced by attachment name, keys in `<code>`, status macros; per-row details with an Old / New / CMS table per language |
| `<slug>.preview.html` | Standalone page with sections 1 Screen, 2 Summary, 3 Details and coloured verdict badges. Absolute image paths become `file://` URLs; bare names stay relative |
| `<slug>.docx` (`--docx`) | Word document built with the standard library: landscape A4, screenshot, overlay and keyshot thumbnails, verdict-shaded cells, a pink cell for drifting CMS TR, right-to-left Arabic rows |
| `<slug>.pdf` (`--pdf`) | LibreOffice from the docx, then headless Chrome from the preview, then `wkhtmltopdf`. With none of them installed the PDF is skipped with a note naming the file to share instead |

`--all` means `--docx --pdf`. Summary columns in order: `#`, UI element, Screenshot, New key, Legacy iOS, Legacy Android, Legacy Web, Old en, Old tr, New en, New tr, CMS TR, CMS EN, Verdict, Note. Detail columns: Lang, Old (legacy), New (redesign), CMS. Verdict colours on Confluence: reuse Green, review Yellow, new Blue, anything else Grey.

A CMS value counts as drifting when both it and `new` are non-empty and they still differ after Unicode NFC normalisation, whitespace collapsing, stripping a trailing `.`, `:` or ellipsis, and lowercasing. Drifting cells carry a warning sign.

The `--ui-lang` switch (`en` default, `tr`) changes headings, legend, labels and column names; keys and values are never translated.

## Overlay and keyshots

- `render-overlay.py` writes `<slug>.overlay.png` (or `<slug>.overlay.html` when no Chrome, Chromium or Edge is installed) and `<slug>.overlay.manifest.json` with `screen`, `file`, `fileKey`, `nodeId`, `cardCount`, `annotatedCount`, `width`, `height`, `scale`. Pink cards carry CMS copy; gray cards show the on-screen text and await copy. `--ui-lang en/tr` (default `en`) sets the card labels.
- `render-key-shots.py` writes `keyshots/keyshot__<NN>__<key>.png` (or `.html` crops with `"fallback": "html"`) and `<slug>.keyshots.manifest.json` with `screen`, `slug`, `pad`, `scale`, `dir`, `shots[]` (`row`, `key`, `element`, `file`, `pageNode`, `cropW`, `cropH`) and `missing[]` (`row`, `key`, `reason`).

## CMS import spreadsheet

`build-spreadsheet.py <map>.json --out <dir>` writes `<slug>.localization.xlsx`, a real Excel workbook built with the standard library (sheet `Localization`, styled and frozen header row, autofilter). `--csv` adds a UTF-8 CSV with BOM; `--csv-only` skips the xlsx. One row per key, attached to the Confluence page at publish time.

Nine fixed columns, spelled exactly like this:

`Channel`, `Property Group`, `Property Module`, `Key`, `EN Value`, `TR Value`, `AR Value`, `Anotation EN`, `Anotation TR`

- `Channel` comes from `--channel` (default `Mobile`).
- `Key` is `newKey` with its trailing marker removed: any parenthesised suffix such as `(owned by ...)` or `(component: ...)`.
- A row whose key is empty after that, starts with `(dynamic`, or contains a `<placeholder>` has no literal key to import: it is left out of the sheet and listed on stderr.
- Values come from `new.en/tr/ar`, annotations from `cms.en/tr`; missing means blank.
- Property Group and Module: row overrides win (each fills its own half). Otherwise the key decides, in order: contains `error` gives Errors; `lookup` gives Lookups; `validation`, `invalid` or a `.valid` ending gives Core / Validation; `field`, `placeholder`, `hint` or `label` gives Core / Fields; a first namespace segment or any word matching a domain list gives the domain group (`Domains`) and the domain name; anything else is Core / Common.
- Default domains: auth (auth, login, signin, sign-in, register, signup, otp, password), account (account, profile, settings, preferences), checkout (checkout, cart, payment, billing, order), search (search, filter, results). A `--taxonomy` JSON object is merged one level deep over these defaults.

## Fidelity gate

`verify-map.py --mapping <map>.json --screen-path <dir> [--resources-root <res>] [--out report.json]` is count reconciliation as a script.

| Check | Hard? |
|---|---|
| Each `newKey` is wired in the screen code (exact match), or exempt as component-owned or dynamic. A chain of the key type (`--key-type`, default `L10n`) counts as a key only with two or more segments, so `extension L10n.Profile` is not one. A key whose last segment only appears as a word is `weak`; neither is `unverified`. When no chain of the key type is found but Swift or Kotlin code has chains rooted at a common accessor name or ending in `.localized`, a `keyTypeWarning` names those roots; set the key type and rerun | unverified: hard |
| Keys the code wires that no row maps (`codeKeysNotInMap`) | hard |
| `reuse` / `review` row without any legacy `en` or `tr` value | hard |
| Non-`new` row with an empty or whitespace-only `new.tr` | soft |
| `new.tr` equal to `new.en` (untranslated) | soft |
| With `--resources-root`: no `Suggested/<Key>.json` in the snapshot or the source layout | soft |

A row is component-exempt when its `newKey` carries a marker naming the owner: `(owned by <Component>)`, `(component ...)` or the Turkish synonym `(bilesen ...)` written with a Turkish s-cedilla (U+015F); the element and note text do not count. A row is dynamic-exempt when its key starts with `(dynamic`, contains a `<placeholder>`, its element mentions dynamic (or the Turkish synonym "dinamik"), or its note says "enumerate at runtime". Folders named `.git` or `Generated` are skipped when scanning; `.github` and similar names are read. Exit 2 when any hard issue exists; publish only after fixing them (drop or fix UNVERIFIED keys, add rows for wired-but-unmapped keys) or after a conscious, stated waiver.

## Script reference

| Script | Key flags |
|---|---|
| `scan-screen-keys.py` | `--screen-path` (required), `--out`, `--category all/error/dynamic/static`, `--key-type` (repeatable or comma list, default `L10n`) |
| `resolve-new-values.py` | `--keys`, `--keys-file` (`-` for stdin), `--resources-root` or `--suggested-dir`, `--domain localization/accessibility`, `--catalog`, `--report-source`, `--langs` (default `all`) |
| `resolve-legacy-values.py` | `--keys` (required), `--langs` (default `all`), `--snapshot-root`, `--live`, `--endpoint`, `--header K=V`, `--headers-file`, `--env`, `--status-field`, `--fail-status CODE=message`, `--timeout`, `--plist-root`, `--prefix` |
| `fetch-legacy-labels.py` | `--out`, `--endpoint` (both required), `--header`, `--headers-file`, `--langs` (default `all`), `--env`, `--status-field`, `--fail-status`, `--timeout` |
| `fetch-annotations.py` | `--file`, `--nodes`, `--mapping`, `--from-mcp`, `--local`, `--source auto/rest/mcp/local`, `--ca-file`, `--out` |
| `render-overlay.py` | `--mapping` (required), `--spec`, `--file`, `--nodes`, `--scale`, `--ca-file`, `--out`, `--slug`, `--ui-lang en/tr` |
| `render-key-shots.py` | `--mapping` (required), `--spec` (repeatable), `--file`, `--nodes`, `--pad` (default 150), `--scale`, `--ca-file`, `--out`, `--slug` |
| `build-artifact.py` | `mapping` (or `-`), `--out`, `--slug`, `--ui-lang en/tr`, `--docx`, `--pdf`, `--all`, `--print-upload`, `--langs` (default `all`) |
| `build-spreadsheet.py` | `mapping` (or `-`), `--out`, `--slug`, `--channel`, `--taxonomy`, `--csv`, `--csv-only` |
| `verify-map.py` | `--mapping`, `--screen-path` (both required), `--resources-root`, `--out`, `--key-type` (else the mapping's `keyType`, else `L10n`) |
| `publish-confluence.py` | see [publish-and-snapshot](publish-and-snapshot.md) |
| `snapshot-resources.sh` | `<resources-root> <specs-root>` |

`--langs` takes a comma list, used in the order given, or `all`: every language present in the data the script reads (the mapping rows, the Suggested files and catalog, the snapshot or plist folder, or for `fetch-legacy-labels.py` the snapshot it refreshes), `en` first and the rest sorted, and `en` alone when the data names none. Every script derives the same slug from `screen`: lowercase, each run of non-letter, non-digit characters becomes one `-`, leading and trailing dashes trimmed, `screen` when nothing is left. `--slug` overrides it on any script. Figma tokens resolve from `FIGMA_ACCESS_TOKEN`, then `FIGMA_TOKEN`, then the macOS keychain item `FIGMA_ACCESS_TOKEN`. `--token` is refused with exit 2, since argv is visible to other processes.
