UI text in Figma is a design artifact. It's mockup copy, often written by designers rather than product managers, rarely reviewed for i18n. figmascope extracts it anyway — because even placeholder copy needs a resource key if you're generating code that will eventually be localized, and the extraction process is where the logic lives.

strings.json is the artifact that maps dot-notation resource IDs to literal string values. This post covers how keys are generated, what gets filtered out, what happens when keys collide, and how this maps to Android strings.xml and other i18n formats.

The format

{
  "speed.test": "Speed Test",
  "data.transfer.rate": "Data Transfer Rate",
  "start": "Start",
  "results.download": "Download",
  "results.upload": "Upload",
  "results.ping": "Ping",
  "unit.mbps": "Mbps",
  "settings.server.auto": "Auto Select"
}

The structure is a flat object keyed by dot-notation resource IDs. Values are the literal string content from Figma. Nested dot paths are semantically meaningful — results.download and results.upload are in the same logical group — but the file is flat JSON, not nested. This matches how Android's strings.xml works (flat resource names with dot convention for grouping) and how most i18n libraries (i18next, Rosetta, etc.) handle flat key namespacing.

How keys are generated

Key generation starts from the node's Figma layer name, not the text content. A text node named Data Transfer Rate Label with content "Data Transfer Rate" generates a key from the layer name, not the value.

The generation pipeline:

  1. Take the layer name
  2. Strip the sanitizeName filters: transliterate Cyrillic characters, remove bidirectional/control characters (Unicode range U+202A–U+202E and similar), remove non-alphanumeric non-space characters
  3. Lowercase and replace spaces with dots
  4. Trim leading and trailing dots

So "Data Transfer Rate Label""data.transfer.rate.label". In practice, designers often name text layers without the trailing noun, so "Data Transfer Rate""data.transfer.rate".

The Cyrillic transliteration step exists because figmascope is used with Figma files across languages. A layer named Скорость transliterates to Skorost', then becomes "skorost'" → after special char strip → "skorost". Not pretty, but the key is stable across runs for the same layer name.

Key generation is from layer name, not text value. Two text nodes with identical text content but different layer names get different keys. Two text nodes with different text content but the same layer name create a collision — handled below.

Key filters — what gets dropped

Not every text node in a Figma file should be a localizable string. figmascope filters before generating keys:

The numeric filter deserves more explanation. A design mock often has placeholder numbers: "42 ms" (ping), "150 Mbps" (download speed). "42 ms" passes the filter because it's not purely numeric — it would become key "42.ms". But you'd probably want to extract just "ms" as a unit label and compute 42 at runtime. figmascope doesn't make that inference — it takes the full text as-is. The short-string filter drops "ms" alone (2 characters), but keeps "42 ms" as a full string.

This is an acknowledged limitation. UI text that mixes template data with literal labels is hard to automatically decompose. The generated code should treat the extracted string as a starting point, not final copy.

Collision handling

A collision occurs when two different text nodes produce the same key but have different values. For example: a layer named "Label" on screen A contains "Download" and a layer named "Label" on screen B contains "Upload". Both generate key "label".

figmascope's collision rule: drop the key entirely from strings.json and emit a warning in _meta.json:

// _meta.json
{
  "warnings": [
    "strings-collision:label"
  ]
}

Keeping one arbitrarily would be wrong — you'd silently translate one of the two strings incorrectly. Keeping both with disambiguated keys (e.g., "label.1", "label.2") would be a guess at naming that serves no one. Dropping is honest.

The IR handles collision cleanly: when a key is dropped due to collision, figmascope walks the IR and removes the stringRef field from all leaf nodes that referenced it. The text field (literal value) remains. The agent sees a leaf with text but no stringRef and knows to use the literal as a fallback — which is safe because the CONTEXT.md rule only says "use stringRef if present."

// Before collision resolution — two leaves with same key, different text
{ "kind": "leaf", "text": "Download", "stringRef": "label" }
{ "kind": "leaf", "text": "Upload",   "stringRef": "label" }

// After collision resolution — key dropped, text preserved
{ "kind": "leaf", "text": "Download" }
{ "kind": "leaf", "text": "Upload"   }

The warning tells you to fix the layer names in Figma. A layer named "Download Label" and "Upload Label" would produce distinct keys "download.label" and "upload.label" with no collision.

Mapping to Android strings.xml

Android strings.xml uses flat string resource IDs with underscores. The dot-notation from figmascope maps cleanly:

// strings.json
{
  "speed.test": "Speed Test",
  "data.transfer.rate": "Data Transfer Rate",
  "results.download": "Download"
}

// → strings.xml (generated)
<resources>
    <string name="speed_test">Speed Test</string>
    <string name="data_transfer_rate">Data Transfer Rate</string>
    <string name="results_download">Download</string>
</resources>

The transformation is: replace dots with underscores. That's the complete transform. Android resource IDs don't allow dots; underscores are the conventional separator. The key speed.test in strings.json becomes R.string.speed_test in Kotlin, which is what the CONTEXT.md constraint points the agent to use when it sees stringRef: "speed.test".

// CONTEXT.md constraint in action
// IR leaf: { "text": "Speed Test", "stringRef": "speed.test" }
// Agent output:
Text(text = stringResource(R.string.speed_test))

Mapping to other i18n formats

Dot-notation is a lingua franca for i18n. Most systems accept it directly or need only minimal transformation:

FormatKey formatTransform from dot-notation
Android strings.xml speed_test Replace . with _
iOS Localizable.strings "speed.test" None (dot-notation used directly)
i18next (JSON) Nested: { "speed": { "test": "..." } } Expand flat keys into nested structure
Flutter ARB speedTest Convert to camelCase

figmascope outputs the dot-notation format. Target-specific transformation is handled downstream — either by the agent (if the CONTEXT.md target is explicit enough) or by a small conversion script.

What strings.json doesn't solve

Honest scope note: strings.json is extracted from whatever text exists in Figma at export time. It's not a validated i18n resource file. Three problems you'll encounter:

Placeholder text: Figma mocks frequently use Lorem Ipsum, "[Title Here]", or designer-authored copy that hasn't been reviewed. The extracted strings are what's in the file, reviewed or not.

Dynamic content: Designs often show "John Doe" or "42 Mbps" as representative data. These aren't localizable strings — they're template values. figmascope can't distinguish design-time data from design-time copy. The numeric filter catches pure numbers, but mixed strings like "42 Mbps" still get extracted.

Missing strings from filtered nodes: If a text node fails a filter (too short, numeric-only, empty after sanitization), it won't appear in strings.json and the IR leaf won't have a stringRef. This is intentional but means your string coverage depends on the quality of layer naming in Figma.

The output is a starting point. Copy it into your i18n file, review it against your actual resource naming conventions, delete the placeholder strings, and add the dynamic content keys manually. That's ten minutes of work instead of manually extracting every text node from Figma. Try extracting strings from your own file on figmascope.dev.

For the full picture of how stringRef flows through the IR into generated code, see Per-Screen IR — Stack, Overlay, Absolute, Leaf. For how the constraints that govern string usage are declared, see Anatomy of CONTEXT.md. For the Jetpack Compose generation workflow end to end, see Jetpack Compose from Figma. Ready to generate your own strings.json? Use the main app at figmascope.dev.