El texto de UI en Figma es un artefacto de diseño. Es copia de maqueta, a menudo escrita por diseñadores en lugar de product managers, raramente revisada para i18n. figmascope la extrae de todos modos — porque incluso el texto de marcador de posición necesita una clave de recurso si estás generando código que eventualmente será localizado, y el proceso de extracción es donde vive la lógica.

strings.json es el artefacto que mapea IDs de recursos en notación de puntos a valores de cadenas literales. Este artículo cubre cómo se generan las claves, qué se filtra, qué sucede cuando las claves colisionan y cómo esto se mapea a Android strings.xml y otros formatos i18n.

El formato

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

La estructura es un objeto plano con clave en IDs de recursos en notación de puntos. Los valores son el contenido de cadena literal de Figma. Las rutas de puntos anidadas son semánticamente significativas — results.download y results.upload están en el mismo grupo lógico — pero el archivo es JSON plano, no anidado. Esto coincide con cómo funciona strings.xml de Android (nombres de recursos planos con convención de puntos para agrupar) y cómo la mayoría de las bibliotecas i18n (i18next, Rosetta, etc.) manejan el espacio de nombres de claves planas.

Cómo se generan las claves

La generación de claves comienza desde el nombre de la capa de Figma del nodo, no del contenido del texto. Un nodo de texto llamado Data Transfer Rate Label con contenido "Data Transfer Rate" genera una clave desde el nombre de la capa, no desde el valor.

El pipeline de generación:

  1. Tomar el nombre de la capa
  2. Aplicar los filtros sanitizeName: transliterar caracteres cirílicos, eliminar caracteres bidireccionales/de control (rango Unicode U+202A–U+202E y similares), eliminar caracteres que no sean alfanuméricos ni espacios
  3. Convertir a minúsculas y reemplazar espacios con puntos
  4. Recortar los puntos iniciales y finales

Entonces "Data Transfer Rate Label""data.transfer.rate.label". En la práctica, los diseñadores a menudo nombran las capas de texto sin el sustantivo final, por lo que "Data Transfer Rate""data.transfer.rate".

El paso de transliteración cirílica existe porque figmascope se usa con archivos de Figma en varios idiomas. Una capa llamada Скорость se transliterada a Skorost', luego se convierte en "skorost'" → después de eliminar caracteres especiales → "skorost". No es bonito, pero la clave es estable entre ejecuciones para el mismo nombre de capa.

La generación de claves es a partir del nombre de la capa, no del valor del texto. Dos nodos de texto con contenido de texto idéntico pero nombres de capa diferentes obtienen claves diferentes. Dos nodos de texto con contenido de texto diferente pero el mismo nombre de capa crean una colisión — manejada a continuación.

Filtros de claves — qué se descarta

No todos los nodos de texto en un archivo de Figma deben ser una cadena localizable. figmascope filtra antes de generar claves:

El filtro numérico merece más explicación. Una maqueta de diseño a menudo tiene números de marcador de posición: "42 ms" (ping), "150 Mbps" (velocidad de descarga). "42 ms" pasa el filtro porque no es puramente numérico — se convertiría en clave "42.ms". Pero probablemente querrías extraer solo "ms" como etiqueta de unidad y calcular 42 en tiempo de ejecución. figmascope no hace esa inferencia — toma el texto completo tal como está. El filtro de cadenas cortas descarta "ms" solo (2 caracteres), pero mantiene "42 ms" como cadena completa.

Esta es una limitación reconocida. El texto de UI que mezcla datos de plantilla con etiquetas literales es difícil de descomponer automáticamente. El código generado debe tratar la cadena extraída como punto de partida, no como copia final.

Manejo de colisiones

Una colisión ocurre cuando dos nodos de texto diferentes producen la misma clave pero tienen valores diferentes. Por ejemplo: una capa llamada "Label" en la pantalla A contiene "Download" y una capa llamada "Label" en la pantalla B contiene "Upload". Ambas generan la clave "label".

La regla de colisión de figmascope: descartar la clave completamente de strings.json y emitir una advertencia en _meta.json:

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

Mantener una arbitrariamente estaría mal — traducirías silenciosamente una de las dos cadenas incorrectamente. Mantener ambas con claves desambiguadas (por ejemplo, "label.1", "label.2") sería una suposición de nomenclatura que no sirve a nadie. Descartar es honesto.

El IR maneja la colisión limpiamente: cuando se descarta una clave por colisión, figmascope recorre el IR y elimina el campo stringRef de todos los nodos leaf que la referenciaban. El campo text (valor literal) permanece. El agente ve un leaf con text pero sin stringRef y sabe que debe usar el literal como fallback — lo cual es seguro porque la regla de CONTEXT.md solo dice "usa stringRef si está presente."

// Antes de la resolución de colisión — dos leaves con la misma clave, texto diferente
{ "kind": "leaf", "text": "Download", "stringRef": "label" }
{ "kind": "leaf", "text": "Upload",   "stringRef": "label" }

// Después de la resolución de colisión — clave descartada, texto preservado
{ "kind": "leaf", "text": "Download" }
{ "kind": "leaf", "text": "Upload"   }

La advertencia te dice que corrijas los nombres de capa en Figma. Una capa llamada "Download Label" y "Upload Label" produciría claves distintas "download.label" y "upload.label" sin colisión.

Mapeo a Android strings.xml

El strings.xml de Android usa IDs de recursos de cadenas planos con guiones bajos. La notación de puntos de figmascope se mapea limpiamente:

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

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

La transformación es: reemplazar puntos con guiones bajos. Esa es la transformación completa. Los IDs de recursos de Android no permiten puntos; los guiones bajos son el separador convencional. La clave speed.test en strings.json se convierte en R.string.speed_test en Kotlin, que es lo que la restricción de CONTEXT.md señala al agente a usar cuando ve stringRef: "speed.test".

// Restricción de CONTEXT.md en acción
// IR leaf: { "text": "Speed Test", "stringRef": "speed.test" }
// Salida del agente:
Text(text = stringResource(R.string.speed_test))

Mapeo a otros formatos i18n

La notación de puntos es una lingua franca para i18n. La mayoría de los sistemas la aceptan directamente o necesitan solo una transformación mínima:

FormatoFormato de claveTransformación desde notación de puntos
Android strings.xml speed_test Reemplazar . con _
iOS Localizable.strings "speed.test" Ninguna (la notación de puntos se usa directamente)
i18next (JSON) Anidado: { "speed": { "test": "..." } } Expandir claves planas en estructura anidada
Flutter ARB speedTest Convertir a camelCase

figmascope produce el formato de notación de puntos. La transformación específica por objetivo se maneja downstream — ya sea por el agente (si el objetivo de CONTEXT.md es lo suficientemente explícito) o por un pequeño script de conversión.

Lo que strings.json no resuelve

Nota honesta de alcance: strings.json se extrae de cualquier texto que exista en Figma en el momento de la exportación. No es un archivo de recursos i18n validado. Tres problemas que encontrarás:

Texto de marcador de posición: Las maquetas de Figma usan con frecuencia Lorem Ipsum, "[Title Here]", o copia escrita por diseñadores que no ha sido revisada. Las cadenas extraídas son las que están en el archivo, revisadas o no.

Contenido dinámico: Los diseños a menudo muestran "John Doe" o "42 Mbps" como datos representativos. Estos no son cadenas localizables — son valores de plantilla. figmascope no puede distinguir datos de tiempo de diseño de copia de tiempo de diseño. El filtro numérico captura números puros, pero cadenas mixtas como "42 Mbps" aún se extraen.

Cadenas faltantes de nodos filtrados: Si un nodo de texto falla un filtro (demasiado corto, solo numérico, vacío después de la sanitización), no aparecerá en strings.json y el leaf IR no tendrá un stringRef. Esto es intencional pero significa que la cobertura de tus cadenas depende de la calidad del nombrado de capas en Figma.

La salida es un punto de partida. Cópiala en tu archivo i18n, revísala contra tus convenciones reales de nomenclatura de recursos, elimina las cadenas de marcador de posición y añade las claves de contenido dinámico manualmente. Eso son diez minutos de trabajo en lugar de extraer manualmente cada nodo de texto de Figma. Prueba extrayendo cadenas de tu propio archivo en figmascope.dev.

Para la imagen completa de cómo fluye stringRef a través del IR hacia el código generado, consulta IR por Pantalla — Stack, Overlay, Absolute, Leaf. Para cómo se declaran las restricciones que gobiernan el uso de cadenas, consulta Anatomía de CONTEXT.md. Para el flujo de trabajo de generación de Jetpack Compose de principio a fin, consulta Jetpack Compose desde Figma. ¿Listo para generar tu propio strings.json? Usa la app principal en figmascope.dev.