AutomationFlowsDevOps › Fill iOS Localization Gaps to Google Sheets

Fill iOS Localization Gaps to Google Sheets

Original n8n title: Fill Ios Localization Gaps From `.strings` → Google Sheets and Pr with Placeholders

Fill iOS localization gaps from `.strings` → Google Sheets and PR with placeholders. Uses httpRequest, googleSheets. Webhook trigger; 11 nodes.

Webhook trigger★★★★☆ complexity11 nodesHTTP RequestGoogle Sheets
DevOps Trigger: Webhook Nodes: 11 Complexity: ★★★★☆ Added:

This workflow follows the Google Sheets → HTTP Request recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "rbB9xa5TQZeGFzXh",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Fill iOS localization gaps from `.strings` \u2192 Google Sheets and PR with placeholders",
  "tags": [],
  "nodes": [
    {
      "id": "7e8fd5f1-3a2c-48e7-9e63-48c9864dfa7a",
      "name": "Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -480,
        -180
      ],
      "parameters": {
        "path": "new-pathss",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "ea60b48c-af71-4440-9aaa-2d25dd820f4f",
      "name": "Config",
      "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": "Process File Tree",
      "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": "Get Source File",
      "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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "3e658165-e4ec-466d-9639-fabe715db1ff",
      "name": "Find Missing Keys",
      "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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "545dcc56-793d-42b1-8321-1eabefcfe720",
      "name": "Get GitHub Tree",
      "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": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "08e3392c-a588-42c2-9c35-6e067d2cf4bb",
      "name": "Merge",
      "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": "Edit Fields",
      "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 Request",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1300,
        -180
      ],
      "parameters": {
        "url": "={{ $json[\"url\"] }}",
        "options": {}
      },
      "typeVersion": 4.2
    },
    {
      "id": "e3729e7f-579f-4c4b-9789-f6a00c7565ed",
      "name": "Code",
      "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,
  "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
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

How this works

This workflow streamlines iOS app localisation by identifying missing translation keys in .strings files, exporting them to Google Sheets for easy team collaboration, and generating a pull request with placeholders to fill the gaps efficiently. It suits developers and localisation managers handling multilingual iOS projects who need to automate repetitive tasks without manual file comparisons. The key step involves processing the file tree via code nodes to detect discrepancies, then leveraging Google Sheets for input before creating a GitHub PR through httpRequest integrations.

Use this workflow when scaling localisation for iOS apps with frequent updates, especially if your team prefers spreadsheet-based editing over direct code changes. Avoid it for non-iOS projects or when translations require complex formatting beyond simple placeholders. Common variations include adapting the code nodes to support Android XML files or integrating with other version control systems like Bitbucket.

About this workflow

Fill iOS localization gaps from `.strings` → Google Sheets and PR with placeholders. Uses httpRequest, googleSheets. Webhook trigger; 11 nodes.

Source: https://github.com/papikimono/n8n-Syncing-iOS-localization-gaps-with-Google-Sheets-and-GitHub-PR-placeholders/blob/main/main.json — original creator credit. Request a take-down →

More DevOps workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

DevOps

Chatgpt Automatic Code Review In Gitlab Mr. Uses stickyNote, splitOut, lmChatOpenAi, httpRequest. Webhook trigger; 14 nodes.

OpenAI Chat, HTTP Request, Chain Llm
DevOps

🔥 n8n Members Sale – n8n Community Members Get ideoGener8r for Just $10! (Reg. $15) Use Coupon Code: (Valid for n8n community members)

n8n, GitHub
DevOps

Code Github. Uses manualTrigger, stickyNote, n8n, httpRequest. Event-driven trigger; 25 nodes.

n8n, HTTP Request, GitHub +1
DevOps

Display Project Data On A Smashing Dashboard. Uses httpRequest, github. Scheduled trigger; 24 nodes.

HTTP Request, GitHub
DevOps

Code Github. Uses manualTrigger, stickyNote, httpRequest, noOp. Event-driven trigger; 24 nodes.

HTTP Request, GitHub, Execute Command +1