从`.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)可能需要您自行付费。
相关工作流推荐
自动化SEO健康监控与报告
网站SEO健康分析(Google Sheets、PDF报告和Gmail告警)
If
Set
Code
+8
16 节点WeblineIndia
市场调研
物联网传感器数据清理器 + InfluxDB 记录器 (n8n | Webhook | Function | InfluxDB)
清理物联网传感器数据并记录到 InfluxDB (Webhook | Function | HTTP)
Set
Webhook
Function
+1
4 节点WeblineIndia
工程
太阳能发电监测预警工作流
监控太阳能发电并通过Gmail、Google表格和Slack发送警报
If
Code
Gmail
+5
9 节点WeblineIndia
工程
从 Figma 同步 Android drawable 资源到 GitHub
通过 PR 将 Android drawable 资源从 Figma 同步至 GitHub(多密度 PNG)
If
Code
Merge
+3
14 节点WeblineIndia
工程
使用 HTTP Last-Modified 检查从 Google Sheets 获取职位发布过期和刷新提醒
通过 Google Sheets、HTTP 检查和 Gmail 实现职位发布过期提醒的自动化
If
Set
Code
+6
19 节点WeblineIndia
人力资源
竞争对手内容差距分析器:自动化网站主题映射
使用Gemini AI、Apify和Google Sheets分析竞争对手内容差距
If
Set
Code
+10
30 节点Mychel Garzon
杂项
工作流信息
难度等级
中级
节点数量11
分类2
节点类型6
作者
WeblineIndia
@weblineindiaA Leading Software Engineering, Consulting & Outsourcing Services Company in USA & India serving Clients Globally since 1999.
外部链接
在 n8n.io 查看 →
分享此工作流