Figma多语言JSON的解决方案:手把手打造React Figma AI Chrome扩展

前言

"每次看到设计师在Figma里复制粘贴文案,我就想写个工具帮他们..."

现实中的开发困境

想象一下这个场景:你是一个前端开发工程师,设计师给你发来一个Figma设计稿,里面有几十个页面,每个页面都有大量的文案内容。而你的业务是面对国际化的,需要生成一个 原始的 JSON 文案去找翻译的同学来帮你翻译成各个国家的语言🤯并且这个 JSON 要根据你具体的项目来调整格式,很好,现在你需要做的任务如下:

  1. 手动复制文案 - 在Figma中一个个点击文本,复制粘贴到代码里
  2. 整理成JSON格式 - 手动整理成前端可用的多语言文件格式
  3. 翻译成多种语言 - 还要找翻译工具逐个翻译
  4. 保持数据结构一致 - 确保所有语言版本的键值对应正确

这个过程不仅效率低下,还容易出错。更要命的是,当设计师修改了文案,你又要重复一遍这个痛苦的过程...这无疑是很不高效的行为

那么这时候,作为一个有"懒癌"的程序员,我开始思考如何提高开发效率:

  • 能否自动从Figma提取文案?
  • 能否直接调用AI进行智能分析和翻译?
  • 能否一键生成前端需要的多语言JSON文件?

于是,这个React Figma AI Chrome扩展项目就诞生了。

💡 从痛点到解决方案

作为一个经常被奇奇怪怪的需求"折磨"的开发者,我深知一个道理:好的工具都是从解决真实痛点开始的。现在让我们把刚才提到的痛点重新梳理一下,但这次我们要从"程序员思维"的角度来分析:

需求1:自动化文案提取

  • 痛点:手动复制粘贴,效率低下,特别是我开发的时候,有时候面对一些游戏规则页(比如一整页的文字说明搭配几个表格的那种),光是复制+整理成完整的 JSON 就已经足够痛苦了,基本上一个内容多的说明页,去创建一个符合规范的 JSON 就需要十分钟左右了😭
  • 解决思路:通过Chrome扩展直接访问Figma API,自动提取页面文案,这样就节省掉我们复制粘贴的时间了

需求2:智能文案处理

  • 痛点:文案需要结构化整理,还要翻译,像往常我开发的时候,除了自己手动去整理以外,最常用的方式就是让 AI 去帮我整理成一个可以开发的JSON,但往往需要手动去输入一大堆的Prompt ,才可以生成一个勉强够用的,或者你会说:"哥们!你弄到备忘录里,需要的时候再弄不就成了吗?",那我问你,这样优雅吗?每次都需要从备忘录里找到Prompt,然后调整项目的描述 or 项目需要的 JSON 格式,这也太繁琐了吧...
  • 解决思路:接入AI服务,让AI帮我们干"脏活累活",最好可以留出调整项目描述和JSON 格式的区域

需求3:一键生成多语言JSON

  • 痛点:手动整理JSON格式,容易出错
  • 解决思路:让AI直接输出标准化的JSON格式,并且支持多语言翻译

那么现在我们的开发思路就已经定好了:

开发流程

初始化项目

bash 复制代码
mkdir figma-analyzer-extension
cd figma-analyzer-extension
npm init -y

开发技术栈选择方面,因为我先前一直是写 Vue 的,对 React 始终保持着好奇,但因为工作原因,一直没有机会去用到这个传奇的前端库,所以这次自己的小项目就选择了 React 来进行开发了,至于打包工具方面,就选择我们熟悉的 Vite 来进行打包构建就好~

除了这些主要的技术栈,我们还要根据我们的项目需求来选择一些有趣又有用的库,belike:

markdown 复制代码
- `@crxjs/vite-plugin`:专为Chrome扩展优化的Vite插件(**开发 chrome extension 的神器!**)
- `@types/chrome`:Chrome扩展API的TypeScript类型定义
- `react-json-pretty`:用来美化显示JSON结果,方便我们直接在插件里浏览 AI 生成的JSON,这个JSON-pretty 足够**轻量美观**

中间的一些细节就省略掉了,如果感兴趣的话可以查看developer.chrome.com/docs/extens... 官方文档,基本上和我们的也大差不多,在稍作调整后,我们的这个项目结构如下

markdown 复制代码
figma-analyzer-extension/
├── src/
│   ├── components/          # React组件
│   │   └── FigmaAnalyzer.tsx
│   ├── manifest.json        # Chrome扩展配置文件(重要!)
│   ├── popup.html          # 扩展弹窗的HTML
│   ├── popup.tsx           # React应用入口
│   ├── popup.css           # 样式文件
│   ├── background.ts       # Service Worker(后台脚本)
│   ├── types.ts            # TypeScript类型定义
│   ├── constants.ts        # 常量定义
│   ├── prompts.ts          # AI提示词模板
│   ├── figmaApi.ts         # Figma API服务
│   └── vite-env.d.ts       # Vite环境类型定义
├── package.json
├── tsconfig.json
├── tsconfig.node.json
├── vite.config.ts
└── README.md

让我稍微做一下解释,这里之所以要prompt 单独放在一个 ts 文件,是因为我们需要根据不同的功能来拆分不同的prompt,并且需要根据用户的具体使用场景,来调整 prompt,如果放在 React 组件中,会让整个项目看上去非常的臃肿,所以这里单独进行拆分了

接入大模型之我全都要

wait?!我们是不是忘记了一个很重要的事情?选择什么大模型呢?如果太贵了会不会得不偿失呢?市面上的大模型也太多了吧...选择困难症了🤡

jsx 复制代码
// 程序员的内心独白
const aiServices = {
  openai: { price: '💰💰💰', quality: '🌟🌟🌟🌟🌟', speed: '🚀🚀🚀' },
  claude: { price: '💰💰', quality: '🌟🌟🌟🌟🌟', speed: '🚀🚀' },
  deepseek: { price: '💰', quality: '🌟🌟🌟🌟', speed: '🚀🚀🚀' },
  ollama: { price: '🆓', quality: '🌟🌟🌟', speed: '🐌' }
};

// 最后决定:小孩才做选择,成年人表示,我全都要!
const solution = '让用户自己选择,我们都支持';

那我们来简单实现一下 AI调用模块

jsx 复制代码
// 支持多种AI服务的统一接口
const callAI = async (prompt, provider) => {
  switch(provider) {
    case 'openai': return await callOpenAI(prompt);
    case 'deepseek': return await callDeepSeek(prompt);
    // ... 其他服务
  }
};

接着让我们实现一下接入 AI 大模型,这里选择性价比最高的 deepseek 来作为示例(其他的大模型同理):

jsx 复制代码
function buildPrompt(request: AIAnalysisRequest): string {
  const { operation, figmaData, projectDescription, targetLanguage } = request;
  
  if (operation === 'translate') {
    // 纯翻译模式
    const textsToTranslate = figmaData.texts.map(t => t.text).join('\n');
    const targetLang = getLanguageName(targetLanguage || 'en');
    const additionalInstruction = `\n\n**最终提醒**:以上共 ${figmaData.texts.length} 行文案,每行输出格式必须是:英文原文:${targetLang}译文`;
    
    return TRANSLATION_PROMPT_TEMPLATE
      .replace(/\{targetLanguage\}/g, targetLang)
      .replace('{textsToTranslate}', textsToTranslate) + additionalInstruction;
      
  } else if (operation === 'translate-and-structure') {
    // 翻译+结构化模式
    const allTextsFormatted = figmaData.texts.map((text, index) => 
      `${index + 1}. "${text.text}"`
    ).join('\n');
    
    const projectDesc = projectDescription || '网页界面设计项目';
    const targetLang = getLanguageName(targetLanguage || 'en');
    const strictReminder = `\n\n**再次强调**:请确保JSON中只包含上述 ${figmaData.totalTextCount} 条提取文案的翻译版本,不要添加任何额外内容!`;
    
    return TRANSLATE_AND_STRUCTURE_PROMPT_TEMPLATE
      .replace(/\{targetLanguage\}/g, targetLang)
      .replace('{textCount}', figmaData.totalTextCount.toString())
      .replace('{allTexts}', allTextsFormatted)
      .replace('{projectDescription}', projectDesc) + strictReminder;
      
  } else {
    // 结构化JSON生成模式
    const allTextsFormatted = figmaData.texts.map(text => text.text).join('\n');
    const projectDesc = projectDescription || '网页界面设计项目';
    const strictReminder = `\n\n**再次强调**:请确保JSON中只包含上述 ${figmaData.totalTextCount} 条提取的文案,不要添加任何额外内容!`;
    
    return ANALYSIS_PROMPT_TEMPLATE
      .replace('{textCount}', figmaData.totalTextCount.toString())
      .replace('{allTexts}', allTextsFormatted)
      .replace('{projectDescription}', projectDesc) + strictReminder;
  }
}

// DeepSeek API调用实现
async function callDeepSeekAPI(prompt: string, apiKey: string): Promise<string> {
  const requestBody = {
    model: 'deepseek-chat',
    messages: [{ role: 'user', content: prompt }],
    temperature: 0.2,  // 降低温度提高一致性
    max_tokens: 2000
  };
  
  console.log('🚀 发送到DeepSeek的请求:', requestBody);
  
  const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(requestBody)
  });

  if (!response.ok) {
    const errorText = await response.text();
    console.error('DeepSeek API Error:', errorText);
    throw new Error(`DeepSeek API错误: ${response.status}`);
  }

  const data = await response.json();
  const content = data.choices[0]?.message?.content;
  
  if (!content) {
    throw new Error('DeepSeek API返回空内容');
  }
  
  return content;
}

接入 Ollama 时,遇见阻碍

当我兴高采烈的接入一个又一个主流大模型的时候,也同时考虑到了这个免费的工具,不仅可以本地调用大模型,还可以保证信息的隐私,来处理一些敏感需求的话,ollama 再合适不过了,但在接入的时候,遇见了第一个大坑:Error 403

在查阅社区的 issues 的时候,我发现了有不少开发者也遇见了同样的问题:github.com/ollama/olla...

bash 复制代码
macOS上:
launchctl setenv OLLAMA_ORIGINS "*"

才刚解决完这个报错,又发现我下载的 deepseek-r1 8b 模型,一直会返回 think 部分:并且这时候,我们的解析接口返回也失效了:

同样的在社区找到了相同的疑问:github.com/deepseek-ai... 官方也并没有在API 文档中说相关的内容...搜索了半天也没有找到结果,于是我尝试去了解 AI 的相关概念,比如

  • stream(流式输出)
  • temperature (控制生成文本随机性的重要参数)
  • think (深度思考)

哦!找到了,在 ollama.com/blog/thinki...

手动将think 设置成 false 即可!顺带一提,我个人不是很喜欢流式输出,即使现在很多的对话式 AI(如 ChatGPT 或者 DeepSeek)都选择了流式输出,但我们还是要根据自己的开发项目来设置,在我们这个需求中,直接获取到最终的结果就行,不需要关注生成的过程

Figma数据获取 - 从注入脚本到REST API的重构之路

最开始我思考的获取 Figma 文本的方式是注入脚本,通过在 Figma 页面中注入 JavaScript 代码来获取选中元素的数据

jsx 复制代码
// 早期的注入脚本方案(已弃用)
function getSelectedElements() {
  // 直接访问Figma的内部API
  const selection = figma.currentPage.selection;
  return selection.map(node => ({
    id: node.id,
    name: node.name,
    text: node.characters
  }));
}

最开始的时候我还沾沾自喜,认为自己的这个实现思路很完美,后面在获取元素的时候,发现经常出现"无法获取选中元素"的错误,这对用户的体验无疑是很差的,这时候,我想到了直接使用Figma API:

jsx 复制代码
export class FigmaApiService {
  private apiToken: string;
  private baseUrl = 'https://api.figma.com/v1';

  constructor(apiToken: string) {
    this.apiToken = apiToken;
  }

  // 获取Figma文件数据
  async getFile(fileId: string): Promise<FigmaFileResponse> {
    const response = await fetch(`${this.baseUrl}/files/${fileId}`, {
      headers: {
        'X-Figma-Token': this.apiToken,
      },
    });

    if (!response.ok) {
      const errorText = await response.text();
      throw new Error(`Figma API 错误 (${response.status}): ${errorText}`);
    }

    return await response.json() as FigmaFileResponse;
  }
}

// 从Figma URL中提取文件ID
static extractFileIdFromUrl(url: string): string | null {
  const patterns = [
    // 匹配 /file/ 或 /design/ 路径
    /(?:www\.)?figma\.com\/(?:file|design)\/([a-zA-Z0-9-_]+)/,
    // 备用模式:更宽松的匹配
    /figma\.com\/[^/]+\/([a-zA-Z0-9-_]+)/
  ];
  
  for (const pattern of patterns) {
    const match = url.match(pattern);
    if (match && match[1]) {
      return match[1];
    }
  }
  return null;
}

// 从URL中提取节点ID(当用户选中元素时)
static extractNodeIdFromUrl(url: string): string | null {
  const nodeIdMatch = url.match(/[?&]node-id=([^&]+)/);
  if (nodeIdMatch) {
    let nodeId = decodeURIComponent(nodeIdMatch[1]);
    nodeId = nodeId.replace('%3A', ':').replace('-', ':');
    return nodeId;
  }
  return null;
}

// 递归提取节点中的所有文案
private extractTextsFromNode(node: FigmaNode, texts: FigmaTextInfo[] = []): FigmaTextInfo[] {
  try {
    // 如果是文本节点且有文案内容
    if (node.type === 'TEXT' && node.characters) {
      const boundingBox = node.absoluteBoundingBox || { x: 0, y: 0, width: 0, height: 0 };
      
      texts.push({
        id: node.id,
        name: node.name,
        text: node.characters,
        fontSize: node.style?.fontSize || 16,
        fontFamily: node.style?.fontFamily || 'Unknown',
        x: boundingBox.x,
        y: boundingBox.y,
        width: boundingBox.width,
        height: boundingBox.height
      });
    }

    // 递归处理子节点
    if (node.children && node.children.length > 0) {
      for (const child of node.children) {
        this.extractTextsFromNode(child, texts);
      }
    }
  } catch (error) {
    console.warn('提取节点文案时出错:', error, node);
  }

  return texts;
}

注意,我们要使用 Figma API 的话,需要使用 Figma API token,这里我们给一个链接,方便用户点击后直接跳转去获取 Figma API Tokenhttps://www.figma.com/developers/api#access-tokens

AI Prompt工程 - 让AI理解你的需求

第一版Prompt的失败经历

最初的Prompt设计得过于简单:

javascript 复制代码
请分析以下文案并生成JSON格式的结果:
{文案内容}

结果AI经常返回格式不规范的内容,有时候还会添加额外的说明文字,即使我将temperature设置的足够低,也有很多奇怪的生成,导致JSON解析失败。

这里补充说明一下:

在 AI(尤其是语言模型)中,"temperature"(温度)是一个控制生成文本随机性的重要参数。


🎲 什么是 Temperature?

每次模型生成下一个 token(词或子词)时,会基于一组 logits (原始分数)通过 Softmax 函数将其转为概率分布。Temperature TTT 会影响 Softmax 的平滑程度: 低温度(T < 1) :概率分布更陡峭,模型更倾向于选择最高概率的词,也就是最"保守""确定"的输出 高温度(T > 1):概率分布更平坦,增加了选择概率较低词的机会,生成更"多样""有创造性"的文本
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> s o f t m a x ( z i / T ) softmax(zi/T) </math>softmax(zi/T)


举个对比例子

在某一句话生成中,如果 logits 是 [2,1,−1][2,1,-1][2,1,−1],Softmax 转换后会是大约 [0.67,0.25,0.08][0.67, 0.25, 0.08][0.67,0.25,0.08]。但如果引入不同Temperature:

  • T → 0:几乎总是选第一个 token,输出高度重复、确定。在日常中,对于技术写作,翻译或者说问答之类的就可以选择这种Temperature
  • T = 1:保留原始分布。
  • T > 1 :分布变平,次优的词也有机会被采样,比如"旁门左道"出现的可能性更高,如果你有天马行空的想法,并且不是很在意会不会出错的话,就可以选择这种

这里关于Temperature的科普我们就讲到这里吧,如果对这个感兴趣的话,可以看 Medium 上的这篇文章,讲的非常详细:medium.com/%40amansing...

回到我们的项目吧,经过大量测试和优化,最终的Prompt模板是这样的:

tsx 复制代码
export const ANALYSIS_PROMPT_TEMPLATE = `
你是一个专业的UI/UX文案分析师,请分析以下 {textCount} 条从设计稿中提取的文案内容。

项目描述:{projectDescription}

需要分析的文案:
{allTexts}

请严格按照以下要求输出JSON格式的分析结果:

1. 必须是有效的JSON格式,不要包含任何其他文字说明
2. 只分析上述提取的 {textCount} 条文案,不要添加额外内容
3. 结构化输出,包含页面标题、描述、建议等字段

输出格式示例:
{
  "__page_title": "页面标题",
  "button_text": "按钮文案",
  "description": "描述文案",
  "title": "主标题"
}

**再次强调**:请确保JSON中只包含上述 {textCount} 条提取的文案,不要添加任何额外内容!
`;

注意!这里只是针对于我的项目来写的内容,如果是你来做的话,可以稍微调整 JSON 格式,也算是一劳永逸的事情了

效果展示

让我们看看最终的页面展示效果,用 Figma 官方的插件入门开发来作为演示:

最后附上一个项目结构图和 github 地址:github.com/isolcat/fig...

如果项目可以帮助到你,欢迎点 star~

graph TD A["用户在Figma中选择元素"] --> B["Chrome扩展Popup界面"] B --> C{"配置AI服务"} C --> D["DeepSeek API"] C --> E["OpenAI API"] C --> F["Claude API"] C --> G["Ollama本地模型"] B --> H["提取Figma数据"] H --> I["Figma REST API"] I --> J{"智能检测模式"} J --> K["从URL提取节点ID"] J --> L["获取整个文件"] K --> M["获取特定元素文案"] L --> N["获取全部文案"] M --> O["Background Script处理"] N --> O O --> P["AI分析处理"] P --> Q["文案翻译"] P --> R["结构化JSON生成"] P --> S["文案分析"] Q --> T["返回结果到Popup"] R --> T S --> T T --> U["用户查看结果"]
相关推荐
duanyuehuan14 分钟前
Vue 组件定义方式的区别
前端·javascript·vue.js
veminhe18 分钟前
HTML5简介
前端·html·html5
洪洪呀19 分钟前
css上下滚动文字
前端·css
哪吒编程1 小时前
我的第一个AI编程助手,IDEA最新插件“飞算JavaAI”,太爽了
java·后端·ai编程
搏博1 小时前
基于Vue.js的图书管理系统前端界面设计
前端·javascript·vue.js·前端框架·数据可视化
掘金安东尼2 小时前
前端周刊第419期(2025年6月16日–6月22日)
前端·javascript·面试
bemyrunningdog2 小时前
AntDesignPro前后端权限按钮系统实现
前端
重阳微噪2 小时前
Data Config Admin - 优雅的管理配置文件
前端
Hilaku2 小时前
20MB 的字体文件太大了,我们把 Icon Font 压成了 10KB
前端·javascript·css
fs哆哆2 小时前
在VB.net中,文本插入的几个自定义函数
服务器·前端·javascript·html·.net