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.download 和 results.upload 在同一个逻辑组中——但文件是扁平 JSON,不是嵌套的。这与 Android 的 strings.xml 工作方式一致(扁平资源名称带点约定用于分组),也与大多数 i18n 库(i18next、Rosetta 等)处理扁平键命名空间的方式一致。
键如何生成
键生成从节点的 Figma 图层名称开始,而不是文本内容。名为 Data Transfer Rate Label 内容为 "Data Transfer Rate" 的文本节点从图层名称生成键,而不是从值生成。
生成管道:
- 取图层名称
- 应用
sanitizeName过滤器:音译西里尔字符,移除双向/控制字符(Unicode 范围 U+202A–U+202E 及类似),移除非字母数字非空格字符 - 小写化并将空格替换为点
- 修剪前导和尾随点
所以 "Data Transfer Rate Label" → "data.transfer.rate.label"。实际上,设计师经常命名文本图层时不带尾随名词,所以 "Data Transfer Rate" → "data.transfer.rate"。
西里尔音译步骤存在是因为 figmascope 在跨语言的 Figma 文件中使用。名为 Скорость 的图层音译为 Skorost',然后变为 "skorost'" → 移除特殊字符后 → "skorost"。不是很好看,但对于相同图层名称,键在每次运行中是稳定的。
键生成来自图层名称,而不是文本值。两个具有相同文本内容但不同图层名称的文本节点获得不同的键。两个具有不同文本内容但相同图层名称的文本节点会产生碰撞——见下文处理方式。
键过滤——什么被丢弃
Figma 文件中并非每个文本节点都应该是可本地化字符串。figmascope 在生成键之前进行过滤:
- 空字符串:没有内容或只有空白内容的文本节点被丢弃。
- 纯数字字符串:像
"42"、"100%"、"3.14"这样的值被丢弃。这些是数据值,不是 UI 文案,应该来自你的数据层。 - 短字符串(≤2 个字符):单字符和双字符字符串被丢弃。图标、项目符号、上标——这些不是字符串资源。
- 净化后为空的键:如果图层名称完全由被
sanitizeName删除的字符组成,结果是空键。这些节点被静默丢弃。
数字过滤值得更多解释。设计模型中经常有占位符数字:"42 ms"(ping)、"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 并从所有引用它的叶节点中移除 stringRef 字段。text 字段(字面值)保留。Agent 看到一个有 text 但没有 stringRef 的叶节点,知道使用字面值作为后备——这是安全的,因为 CONTEXT.md 规则只说"如果存在则使用 stringRef"。
// 碰撞解决前——两个叶节点有相同的键,不同的文本
{ "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" 时指向 Agent 使用的内容。
// CONTEXT.md 约束实际应用
// IR 叶节点:{ "text": "Speed Test", "stringRef": "speed.test" }
// Agent 输出:
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 |
转换为驼峰命名法 |
figmascope 输出点记法格式。特定目标的转换在下游处理——无论是由 Agent(如果 CONTEXT.md 目标足够明确)还是由小型转换脚本。
strings.json 不解决什么
诚实的范围说明:strings.json 是从导出时 Figma 中存在的任何文本提取的。它不是经过验证的 i18n 资源文件。你会遇到三个问题:
占位符文本:Figma 模型中经常使用 Lorem Ipsum、"[Title Here]" 或设计师撰写的尚未审查的文案。提取的字符串就是文件中的内容,无论是否经过审查。
动态内容:设计中经常显示 "John Doe" 或 "42 Mbps" 作为代表性数据。这些不是可本地化字符串——它们是模板值。figmascope 无法区分设计时数据和设计时文案。数字过滤器捕获纯数字,但像 "42 Mbps" 这样的混合字符串仍然会被提取。
过滤节点的缺失字符串:如果文本节点未通过过滤器(太短、纯数字、净化后为空),它不会出现在 strings.json 中,IR 叶节点也不会有 stringRef。这是有意为之,但意味着你的字符串覆盖率取决于 Figma 中图层命名的质量。
输出是一个起点。将其复制到你的 i18n 文件中,根据你实际的资源命名约定审查它,删除占位符字符串,并手动添加动态内容键。这是十分钟的工作,而不是从 Figma 手动提取每个文本节点。在 figmascope.dev 上尝试从你自己的文件提取字符串。
有关 stringRef 如何通过 IR 流入生成代码的完整情况,请参阅 每屏 IR — Stack、Overlay、Absolute、Leaf。有关管理字符串使用的约束如何声明,请参阅 CONTEXT.md 解析。有关完整的 Jetpack Compose 生成工作流,请参阅 从 Figma 生成 Jetpack Compose。准备好生成你自己的 strings.json 了吗?使用 figmascope.dev 主应用。