Figma의 UI 텍스트는 디자인 아티팩트입니다. 제품 관리자가 아닌 디자이너가 작성하는 경우가 많고, i18n 검토가 거의 없는 목업 카피입니다. figmascope는 어차피 그것을 추출합니다 — 왜냐하면 결국 현지화될 코드를 생성하는 경우, 심지어 플레이스홀더 카피도 리소스 키가 필요하며, 추출 과정이 로직이 있는 곳이기 때문입니다.

strings.json은 점 표기법 리소스 ID를 리터럴 문자열 값에 매핑하는 아티팩트입니다. 이 글은 키가 어떻게 생성되고, 무엇이 필터링되며, 키가 충돌할 때 무슨 일이 일어나고, 이것이 Android strings.xml 및 기타 i18n 형식에 어떻게 매핑되는지 다룹니다.

형식

{
  "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"
}

구조는 점 표기법 리소스 ID로 키가 지정된 평탄한 객체입니다. 값은 Figma의 리터럴 문자열 콘텐츠입니다. 중첩된 점 경로는 의미론적으로 의미 있습니다 — results.downloadresults.upload는 동일한 논리 그룹에 있습니다 — 하지만 파일은 중첩이 아닌 평탄한 JSON입니다. 이것은 Android의 strings.xml이 작동하는 방식(그룹화를 위한 점 규칙을 사용하는 평탄한 리소스 이름)과 대부분의 i18n 라이브러리(i18next, Rosetta 등)가 평탄한 키 네임스페이싱을 처리하는 방식과 일치합니다.

키가 생성되는 방법

키 생성은 텍스트 콘텐츠가 아닌 노드의 Figma 레이어 이름에서 시작됩니다. 콘텐츠 "Data Transfer Rate"를 가진 Data Transfer Rate Label이라는 텍스트 노드는 값이 아닌 레이어 이름에서 키를 생성합니다.

생성 파이프라인:

  1. 레이어 이름 가져오기
  2. sanitizeName 필터 적용: 키릴 문자 음역, 양방향/제어 문자(유니코드 범위 U+202A–U+202E 등) 제거, 알파벳/숫자/공백이 아닌 문자 제거
  3. 소문자로 변환하고 공백을 점으로 교체
  4. 앞뒤 점 제거

그래서 "Data Transfer Rate Label""data.transfer.rate.label". 실제로, 디자이너들은 종종 뒤에 오는 명사 없이 텍스트 레이어 이름을 붙이므로, "Data Transfer Rate""data.transfer.rate".

키릴 음역 단계는 figmascope가 여러 언어의 Figma 파일과 함께 사용되기 때문에 있습니다. Скорость라는 레이어 이름은 Skorost'로 음역되고, 그런 다음 "skorost'"가 됩니다 → 특수 문자 제거 후 → "skorost". 예쁘진 않지만, 키는 동일한 레이어 이름에 대해 실행 간에 안정적입니다.

키 생성은 텍스트 값이 아닌 레이어 이름에서 합니다. 동일한 텍스트 콘텐츠를 가지지만 다른 레이어 이름을 가진 두 텍스트 노드는 다른 키를 얻습니다. 다른 텍스트 콘텐츠를 가지지만 동일한 레이어 이름을 가진 두 텍스트 노드는 충돌을 만듭니다 — 아래에서 처리됩니다.

키 필터 — 제외되는 것

Figma 파일의 모든 텍스트 노드가 현지화 가능한 문자열이 되어야 하는 것은 아닙니다. figmascope는 키를 생성하기 전에 필터링합니다:

숫자 필터는 더 많은 설명이 필요합니다. 디자인 목업에는 종종 플레이스홀더 숫자가 있습니다: "42 ms" (핑), "150 Mbps" (다운로드 속도). "42 ms"는 순수하게 숫자가 아니기 때문에 필터를 통과합니다 — 키 "42.ms"가 됩니다. 하지만 아마도 "ms"만 단위 레이블로 추출하고 42를 런타임에 계산하고 싶을 것입니다. figmascope는 그 추론을 하지 않습니다 — 전체 텍스트를 그대로 가져옵니다. 짧은 문자열 필터는 "ms" 단독을 제외하지만(2자), "42 ms"는 전체 문자열로 유지합니다.

이것은 인정된 한계입니다. 리터럴 레이블과 템플릿 데이터를 혼합하는 UI 텍스트는 자동으로 분해하기 어렵습니다. 생성된 코드는 추출된 문자열을 최종 카피가 아닌 시작점으로 취급해야 합니다.

충돌 처리

충돌은 두 개의 다른 텍스트 노드가 동일한 키를 생성하지만 다른 값을 가질 때 발생합니다. 예: 화면 A의 "Label"이라는 레이어는 "Download"를 포함하고 화면 B의 "Label"이라는 레이어는 "Upload"를 포함합니다. 둘 다 키 "label"을 생성합니다.

figmascope의 충돌 규칙: strings.json에서 키를 완전히 제외하고 _meta.json에 경고를 발생시킵니다:

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

하나를 임의로 유지하는 것은 잘못된 것입니다 — 두 문자열 중 하나를 조용히 잘못 번역하게 됩니다. 구별된 키로 둘 다 유지하는 것(예: "label.1", "label.2")은 아무에게도 도움이 안 되는 명명에 대한 추측입니다. 제외하는 것이 솔직합니다.

IR은 충돌을 깔끔하게 처리합니다: 충돌로 인해 키가 제외될 때, figmascope는 IR을 순회하여 그것을 참조하는 모든 leaf 노드에서 stringRef 필드를 제거합니다. text 필드(리터럴 값)는 남습니다. 에이전트는 text는 있지만 stringRef가 없는 leaf를 보고 리터럴을 폴백으로 사용해야 함을 알게 됩니다 — CONTEXT.md 규칙이 "있으면 stringRef를 사용하세요"라고만 하기 때문에 이것은 안전합니다.

// 충돌 해결 전 — 동일한 키, 다른 텍스트를 가진 두 leaf
{ "kind": "leaf", "text": "Download", "stringRef": "label" }
{ "kind": "leaf", "text": "Upload",   "stringRef": "label" }

// 충돌 해결 후 — 키 제외, 텍스트 유지
{ "kind": "leaf", "text": "Download" }
{ "kind": "leaf", "text": "Upload"   }

경고는 Figma에서 레이어 이름을 수정하도록 알려줍니다. "Download Label""Upload Label"이라는 레이어는 충돌 없이 고유한 키 "download.label""upload.label"을 생성합니다.

Android strings.xml에 매핑

Android strings.xml은 밑줄이 있는 평탄한 문자열 리소스 ID를 사용합니다. figmascope의 점 표기법은 깔끔하게 매핑됩니다:

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

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

변환은: 점을 밑줄로 교체합니다. 그것이 완전한 변환입니다. Android 리소스 ID는 점을 허용하지 않습니다; 밑줄이 관례적인 구분자입니다. strings.json의 키 speed.test는 Kotlin에서 R.string.speed_test가 됩니다. 이것이 CONTEXT.md 제약이 에이전트에게 stringRef: "speed.test"를 볼 때 사용하도록 지시하는 것입니다.

// CONTEXT.md 제약 적용 중
// IR leaf: { "text": "Speed Test", "stringRef": "speed.test" }
// 에이전트 출력:
Text(text = stringResource(R.string.speed_test))

다른 i18n 형식에 매핑

점 표기법은 i18n의 공통 언어입니다. 대부분의 시스템은 직접 수락하거나 최소한의 변환만 필요합니다:

형식키 형식점 표기법에서 변환
Android strings.xml speed_test ._로 교체
iOS Localizable.strings "speed.test" 없음 (점 표기법 직접 사용)
i18next (JSON) 중첩: { "speed": { "test": "..." } } 평탄한 키를 중첩 구조로 확장
Flutter ARB speedTest camelCase로 변환

figmascope는 점 표기법 형식을 출력합니다. 대상별 변환은 다운스트림에서 처리됩니다 — 에이전트(CONTEXT.md 대상이 충분히 명시적인 경우)나 작은 변환 스크립트로.

strings.json이 해결하지 않는 것

솔직한 범위 주의: strings.json은 내보내기 시점에 Figma에 있는 텍스트에서 추출됩니다. 검증된 i18n 리소스 파일이 아닙니다. 세 가지 문제를 만날 것입니다:

플레이스홀더 텍스트: Figma 목업에는 자주 Lorem Ipsum, "[Title Here]", 또는 검토되지 않은 디자이너 작성 카피를 사용합니다. 추출된 문자열은 파일에 있는 것으로, 검토 여부와 관계없이.

동적 콘텐츠: 디자인은 종종 대표적인 데이터로 "John Doe""42 Mbps"를 보여줍니다. 이것들은 현지화 가능한 문자열이 아닙니다 — 템플릿 값입니다. figmascope는 디자인 타임 데이터와 디자인 타임 카피를 구분할 수 없습니다. 숫자 필터는 순수한 숫자를 잡지만, "42 Mbps"와 같은 혼합 문자열은 여전히 추출됩니다.

필터링된 노드에서 누락된 문자열: 텍스트 노드가 필터를 통과하지 못하면(너무 짧음, 숫자만, 정규화 후 비어 있음), strings.json에 나타나지 않으며 IR leaf에 stringRef가 없습니다. 의도적이지만 문자열 커버리지가 Figma의 레이어 명명 품질에 달려 있음을 의미합니다.

출력은 시작점입니다. i18n 파일에 복사하고, 실제 리소스 명명 규칙에 대해 검토하며, 플레이스홀더 문자열을 삭제하고, 동적 콘텐츠 키를 수동으로 추가하세요. Figma에서 모든 텍스트 노드를 수동으로 추출하는 대신 10분의 작업입니다. figmascope.dev에서 자신의 파일로 문자열 추출을 시도해보세요.

stringRef가 IR을 통해 생성된 코드로 흐르는 전체 그림은 화면별 IR — Stack, Overlay, Absolute, Leaf를 참조하세요. 문자열 사용을 규정하는 제약이 선언되는 방법은 CONTEXT.md 해부를 참조하세요. Jetpack Compose 생성 워크플로 처음부터 끝까지는 Figma로 Jetpack Compose 만들기를 참조하세요. 자신의 strings.json을 생성할 준비가 되셨나요? figmascope.dev의 메인 앱을 사용하세요.