8
n8n 中文网amn8n.com

从`.strings`文件填充iOS本地化缺失项→Google Sheets并提交带占位符的PR

中级

这是一个Engineering, Multimodal AI领域的自动化工作流,包含 11 个节点。主要使用 Set, Code, Merge, Webhook, HttpRequest 等节点。 将iOS本地化缺失项与Google Sheets和GitHub PR占位符同步

前置要求
  • HTTP Webhook 端点(n8n 会自动生成)
  • 可能需要目标 API 的认证凭证
  • Google Sheets API 凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "rbB9xa5TQZeGFzXh",
  "meta": {
    "instanceId": "14e4c77104722ab186539dfea5182e419aecc83d85963fe13f6de862c875ebfa",
    "templateCredsSetupCompleted": true
  },
  "name": "从 `.strings` 文件填充 iOS 本地化缺失项 → Google Sheets 并提交带占位符的 PR",
  "tags": [],
  "nodes": [
    {
      "id": "7e8fd5f1-3a2c-48e7-9e63-48c9864dfa7a",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -480,
        -180
      ],
      "webhookId": "d42cc1fa-7fb4-4ab6-93ec-9195d6755cc9",
      "parameters": {
        "path": "new-pathss",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "ea60b48c-af71-4440-9aaa-2d25dd820f4f",
      "name": "配置",
      "type": "n8n-nodes-base.set",
      "position": [
        -240,
        -220
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "10855098-beb5-49c8-bb4c-e44640ee33b1",
              "name": "GITHUB_OWNER",
              "type": "string",
              "value": "github_user_name"
            },
            {
              "id": "d0637d54-9b4f-4f83-b09c-9a55f3bc243e",
              "name": "GITHUB_REPO",
              "type": "string",
              "value": "n8n-iOS-Github-repo"
            },
            {
              "id": "df285f8f-dde8-42ed-840b-7eb9219aae16",
              "name": "BASE_BRANCH",
              "type": "string",
              "value": "main"
            },
            {
              "id": "2cedf437-f4a5-4be8-830d-cd4d747dbb91",
              "name": "SOURCE_LANG",
              "type": "string",
              "value": "en"
            },
            {
              "id": "679c6a49-cc7f-45c0-b87f-a38bd36f6c85",
              "name": "TARGET_LANG",
              "type": "string",
              "value": "fr"
            },
            {
              "id": "114d96e4-0ef7-4bb6-b52c-a55575730141",
              "name": "PLACEHOLDER_VALUE",
              "type": "string",
              "value": "__TODO_TRANSLATE__"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "8f5566bc-59de-44fe-b370-a7039de6f926",
      "name": "处理文件树",
      "type": "n8n-nodes-base.code",
      "position": [
        220,
        -180
      ],
      "parameters": {
        "jsCode": "// Extract .strings files from GitHub tree\nconst tree = $input.first().json.tree;\nconst sourceFiles = [];\nconst targetFiles = [];\nconst targetLang = $('Config').first().json.TARGET_LANG;\n\n// Find Base.lproj and en.lproj files (source)\ntree.forEach(file => {\n  if ((file.path.includes('Base.lproj/') || file.path.includes('en.lproj/')) && file.path.endsWith('.strings')) {\n    // Extract directory correctly - remove the language folder and filename\n    let directory = file.path.replace(/\\/(Base|en)\\.lproj\\/[^\\/]+$/, '');\n    if (directory === file.path) {\n      // If no replacement happened, it's in root\n      directory = '';\n    }\n    \n    sourceFiles.push({\n      path: file.path,\n      sha: file.sha,\n      basename: file.path.split('/').pop(),\n      directory: directory\n    });\n  }\n});\n\n// Find target language files (fr.lproj)\ntree.forEach(file => {\n  if (file.path.includes(`${targetLang}.lproj/`) && file.path.endsWith('.strings')) {\n    let directory = file.path.replace(new RegExp(`\\\\/${targetLang}\\\\.lproj\\\\/[^\\\\/]+$`), '');\n    if (directory === file.path) {\n      directory = '';\n    }\n    \n    targetFiles.push({\n      path: file.path,\n      sha: file.sha,\n      basename: file.path.split('/').pop(),\n      directory: directory\n    });\n  }\n});\n\n// Create pairs of source and target files\nconst filePairs = [];\nsourceFiles.forEach(sourceFile => {\n  const targetFile = targetFiles.find(tf => \n    tf.basename === sourceFile.basename && tf.directory === sourceFile.directory\n  );\n  \n  // Construct correct target path\n  let targetPath;\n  if (sourceFile.directory === '') {\n    targetPath = `${targetLang}.lproj/${sourceFile.basename}`;\n  } else {\n    targetPath = `${sourceFile.directory}/${targetLang}.lproj/${sourceFile.basename}`;\n  }\n  \n  filePairs.push({\n    source: sourceFile,\n    target: targetFile || null,\n    targetPath: targetPath\n  });\n});\n\nconsole.log(`Found ${sourceFiles.length} source files, ${targetFiles.length} target files`);\nconsole.log('File pairs:', filePairs);\n\nreturn filePairs.map(pair => ({ json: pair }));"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "ca005412-11bf-4630-bc19-d29b3916074d",
      "name": "获取源文件",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        480,
        -180
      ],
      "parameters": {
        "url": "{{\"https://api.github.com/repos/github-user-name/n8n-iOS-Github-repo/contents/\" + $json.source.path + \"?ref=main\"}}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi"
      },
      "credentials": {
        "githubApi": {
          "id": "DOVvxrvTQXiWu4kb",
          "name": "GitHub account 5"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "3e658165-e4ec-466d-9639-fabe715db1ff",
      "name": "查找缺失键",
      "type": "n8n-nodes-base.code",
      "position": [
        1780,
        -200
      ],
      "parameters": {
        "jsCode": "function parseStringsFile(content) {\n  const keys = {};\n  if (!content) return keys;\n  \n  try {\n    const decoded = Buffer.from(content, 'base64').toString('utf-8');\n    const lines = decoded.split('\\n');\n    \n    for (const line of lines) {\n      const match = line.match(/^\\s*\"([^\"]+)\"\\s*=\\s*\"([^\"]*)\";?/);\n      if (match) {\n        keys[match[1]] = match[2];\n      }\n    }\n  } catch (error) {\n    console.log('Error parsing content:', error);\n  }\n  \n  return keys;\n}\n\nconst results = [];\nconst placeholder = '__TODO_TRANSLATE__';\n\nconsole.log('=== DEBUGGING MERGED DATA ===');\nconsole.log('Total items received:', items.length);\n\n// Log all items to understand the structure\nitems.forEach((item, index) => {\n  console.log(`Item ${index}:`);\n  console.log('  - name:', item.json.name);\n  console.log('  - path:', item.json.path);\n  console.log('  - html_url:', item.json.html_url);\n  console.log('---');\n});\n\n// Find source file (Base.lproj or en.lproj)\nconst sourceFile = items.find(item => \n  item.json.path && (\n    item.json.path.includes('Base.lproj/Localizable.strings') ||\n    item.json.path.includes('en.lproj/Localizable.strings')\n  )\n);\n\n// Find target file (fr.lproj or your target locale)\nconst targetFile = items.find(item => \n  item.json.path && item.json.path.includes('fr.lproj/Localizable.strings')\n);\n\nconsole.log('Source file found:', !!sourceFile);\nconsole.log('Target file found:', !!targetFile);\n\nif (sourceFile && targetFile) {\n  console.log('Source path:', sourceFile.json.path);\n  console.log('Target path:', targetFile.json.path);\n  \n  const sourceKeys = parseStringsFile(sourceFile.json.content);\n  const targetKeys = parseStringsFile(targetFile.json.content);\n  \n  console.log('Source keys:', Object.keys(sourceKeys).length);\n  console.log('Target keys:', Object.keys(targetKeys).length);\n  \n  // Find missing keys\n  Object.keys(sourceKeys).forEach(key => {\n    if (!targetKeys.hasOwnProperty(key)) {\n      results.push({\n        json: {\n          File: sourceFile.json.name,\n          \"Source Path\": sourceFile.json.path,\n          \"Target Path\": targetFile.json.path,\n          Key: key,\n          \"Source Value\": sourceKeys[key],\n          Placeholder: placeholder\n        }\n      });\n    }\n  });\n  \n  console.log('Missing keys found:', results.length);\n} else {\n  console.log('ERROR: Could not find both files!');\n}\n\nreturn results.length > 0 ? results : [{\n  json: {\n    File: \"DEBUG\",\n    \"Source Path\": sourceFile ? sourceFile.json.path : \"Not found\",\n    \"Target Path\": targetFile ? targetFile.json.path : \"Not found\",\n    Key: \"debug\",\n    \"Source Value\": `Total items: ${items.length}`,\n    Placeholder: placeholder\n  }\n}];"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "936bf8f5-17af-4ab6-86bc-7d869008464e",
      "name": "Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2000,
        -200
      ],
      "parameters": {
        "columns": {
          "value": {
            "Key": "={{ $json[\"Key\"] }}",
            "File": "={{ $json[\"File\"] }}",
            "Placeholder": "={{ $json[\"Placeholder\"] }}",
            "Source Path": "={{ $json[\"Source Path\"] }}",
            "Target Path": "={{ $json[\"Target Path\"] }}",
            "Source Value": "={{ $json[\"Source Value\"] }}"
          },
          "schema": [
            {
              "id": "File",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "File",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Source Path",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Source Path",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Target Path",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Target Path",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Key",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Key",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Source Value",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Source Value",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Placeholder",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Placeholder",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "localize"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "1n_AIqOd10Q0ErQZSO4q4LBMekwgsR4cP7EW2q9nEzdk"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "K51JQLS2WHFWzap5",
          "name": "Google Sheets account 7"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "545dcc56-793d-42b1-8321-1eabefcfe720",
      "name": "获取 GitHub 文件树",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -20,
        -180
      ],
      "parameters": {
        "url": "={{\"https://api.github.com/repos/\" + $json.GITHUB_OWNER + \"/\" + $json.GITHUB_REPO + \"/git/trees/\" + $json.BASE_BRANCH + \"?recursive=1\"}}",
        "options": {},
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "githubApi"
      },
      "credentials": {
        "githubApi": {
          "id": "DOVvxrvTQXiWu4kb",
          "name": "GitHub account 5"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "08e3392c-a588-42c2-9c35-6e067d2cf4bb",
      "name": "合并",
      "type": "n8n-nodes-base.merge",
      "position": [
        1540,
        -180
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3.2,
      "alwaysOutputData": false
    },
    {
      "id": "1a0f5915-36a8-40a9-bcda-9c57f4bb1a94",
      "name": "编辑字段",
      "type": "n8n-nodes-base.set",
      "position": [
        760,
        -180
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "10c2ff16-6b8e-4c52-ad0f-0245061bd808",
              "name": "en.lproj",
              "type": "string",
              "value": "https://api.github.com/repos/github-user-name/n8n-iOS-Github-repo/contents/en.lproj/Localizable.strings"
            },
            {
              "id": "8e14c09b-d149-4a4e-88ba-c700ad6f6d52",
              "name": "fr.lproj",
              "type": "string",
              "value": "https://api.github.com/repos/github-user-name/n8n-iOS-Github-repo/contents/fr.lproj/Localizable.strings"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "0a4cb729-fd21-442a-b59d-3c313e0694ef",
      "name": "HTTP 请求",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1300,
        -180
      ],
      "parameters": {
        "url": "={{ $json[\"url\"] }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "e3729e7f-579f-4c4b-9789-f6a00c7565ed",
      "name": "代码",
      "type": "n8n-nodes-base.code",
      "position": [
        1080,
        -180
      ],
      "parameters": {
        "jsCode": "return [\n  {\n    json: {\n      lang: \"en\",\n      url: $json[\"en\"][\"lproj\"]\n    }\n  },\n  {\n    json: {\n      lang: \"fr\",\n      url: $json[\"fr\"][\"lproj\"]\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "b0f3ff39-117a-4ee1-884f-419d55594f24",
  "connections": {
    "Code": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Find Missing Keys",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Config": {
      "main": [
        [
          {
            "node": "Get GitHub Tree",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook": {
      "main": [
        [
          {
            "node": "Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Get GitHub Tree": {
      "main": [
        [
          {
            "node": "Process File Tree",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Source File": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Missing Keys": {
      "main": [
        [
          {
            "node": "Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process File Tree": {
      "main": [
        [
          {
            "node": "Get Source File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

中级 - 工程, 多模态 AI

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
中级
节点数量11
分类2
节点类型6
难度说明

适合有一定经验的用户,包含 6-15 个节点的中等复杂度工作流

作者
WeblineIndia

WeblineIndia

@weblineindia

A Leading Software Engineering, Consulting & Outsourcing Services Company in USA & India serving Clients Globally since 1999.

外部链接
在 n8n.io 查看

分享此工作流