← → · N notes · Cmd/Ctrl-P to PDF
Motorola Solutions · Localization

How to capture localization screenshots automatically, at scale

The idea · the build · the limit
Approach 1
failed
Map every string to a component, photograph every component, and let each string inherit its owner’s evidence.
The silver bullet

Natural ownership

resource file
string
string
string
owned by exactly one
component
owns one or more
evidence
screenshot
screenshot
screenshot
1
Link one

string → component

en.json
{
  "Pages": {
    "analytics": {
      "dashboard": {
        "title": "Analytics"
} } } }
flatten
namespaced dotted key
Pages.analytics.dashboard
component scope
.title
leaf
then resolve the key against the source
1find every call site that resolves the key
2call site → component, via imports and the render tree
3component → route, via the router
emit
what comes out
{
  "screenName": "NetworkSettingsPage",
  "route": "/network/settings",
  "id_leaf_index": { "dhcpToggle_Label": [ {
    "full_id": "network:Pages.network.settings.dhcp.label",
    "category": "page_child",
    "catalog_verified": true
  } ] }
}
2
Link two

component → screenshot

By now every string has an owner. What we still don’t have is a way to get that component on screen — so a model reads its source and writes the steps.
component
LLM reads the source
capture script
screenshot
one screenshot, emitted per component
{
  "screenshot_id": "s017_network_firewall_rules_ConfirmDialog",
  "components": ["src/components/shared/ConfirmDialog.tsx"],
  "steps": [
    { "navigate": "/network/firewall/rules" },
    { "click": "t:rules.add_button" },
    { "click": "setOpen" }
  ],
  "expected_strings": ["Close", "Confirm", "Cancel"],
  "photo": "s017_..._ConfirmDialog.png"
}
the model only gets these verbs
navigatego straight to a route clickby exact accessible name, never substring expandtabs · aria-expanded · dialog openers assertthe strings that must end up on screen
A thin Playwright wrapper — the model composes steps from this vocabulary, it never writes browser code. Bad steps fail loudly: if expected_strings do not appear, the shot is not coverage.
3
The limit

One link is broken

string
proven
component
the break
screenshot
1

Most components carry almost no strings

We paid roughly the same to cover every component and every route — but the strings are concentrated in a few screens. The long tail cost the same and bought almost nothing.

2

The steps cannot be read out of the source

Reaching a component often needs a login state, particular hardware, or the device in a particular status. None of that is written in the code, so neither a static pass nor the model can discover it — and the real codebase is all edge cases.

The effort was spread evenly. The value never was.
4
Approach 2
in use
Walk the device, capture what is really there, and account for every string — covered or not.
Phase 1 & 2

Walk the device, build the tree

entry sign in with DEVICE_USER / DEVICE_PASS from .env
General shot
Network shot
sub-tab shot
dialog shot
Image & Display shot
Motion Detection shot
+ 18 more tabs shot each
assert on every hop
1 traverse record every navigable tab, enter it, look for sub-tabs and interactive elements, recurse
2 capture one shot per node, fixed naming convention — the set is addressable, not a folder of images
assert every hop is asserted, so a broken node shows up at that node, not as a pile at the end
5
Phase 3

Assign, then prove it

assert every hop
LLM assigns strings
OCR verifies
Assertions fire on every node-to-node hop, so a broken node shows up at that node instead of as a pile at the end. The key already carries component context, so assignment is tractable. OCR is what turns an assignment into evidence.
when OCR says the string is not there

Wrong screenshot

the assignment was off — reassign it to the shot it actually appears in

Missing path

the traversal never went there — extend coverage to reach it

Conditional

it only renders under a specific state — move it to its own bucket

6
The contract

Every key is in a bucket

screenshotContract.v1.yaml
meta:
  id: cct-localization-screenshot-contract
  version: v1        locale: en
  source: manual-source-derived
  scope: current-manifest-known-keys
  translationFile: apps/renderer/public/locales/en/translation.json
  notes:
    - deterministic source of truth for key-to-screenshot ownership
    - compiled from keys listed under screenshots;
      rendered text is verification only
    - a translation key may appear in exactly one screenshot entry
    - every named manifest capture must appear in this contract
no key twice — exactly one screenshot entry
nothing orphaned — every capture accounted for
coverage · unassignedKeysClassification
INTENDED_TO_COVER:
  reason: surfaces we still intend to cover — the flow,
    state setup, or ownership is incomplete
  buckets:
    REQUIRES_DEDICATED_STATE_FLOWS:
      reason: valid surfaces, but need state setup
      keys: [ NO_CHANGES_DETECTED, FILE_PATH,
              CONFIRM_DELETE_PROJECT_MESSAGE, ... ]
    REVIEW_AND_COMMAND_STATUS_BRANCH_FLOW_CANDIDATES:
      keys: [ CLOSE, IN_PROGRESS, TASK_DETAILS,
              UPGRADE_FIRMWARE, 'YES', 'NO', ... ]
    CLIENT_SETTINGS_AND_USER_PRESENCE_SURFACES:
      keys: [ CLIENT_SETTINGS_FIREWALL_RULE_CREATED, ... ]
    DEVICE_STATUS_AND_PREVIEW_STATES: ...
~60% covered — on a main path, shot, OCR-confirmed
~20% intended, not yet — we take the high-ROI buckets
rest not covering — every bucket carries its reason
target ~80%
7
The deliverable

One string, one screenshot

the string

~1,000 strings go out for translation. Each one ships with its own screenshot, with that string circled.

for usverification is a glance, not an investigation for themthe translator finally sees where the words live

If the same string appears twice on one screen we circle one of them. This is not a perfection exercise, and the case is rare.

8
Where this goes

Weeks to days

Any web UI

Nothing here is camera specific. It needs a reachable web UI, a resource file, and an environment we can prepare — which is most of what we ship.

weeks
days

We stopped trying to find every screen. We started accounting for every string. That is what made it cheap enough to repeat.

9
Speaker notes