Tutorial

How to fail CI when a translation key goes missing

2026-06-10 · 6 min read

A missing translation key is the cheapest bug to detect and one of the most embarrassing to ship. Your German checkout renders checkout.button.label as literal text, a customer screenshots it, and you find out from support. Nothing in a typical stack catches this: the unit tests pass, the type checker is happy, lint has no opinion about your locale files.

The fix is mechanical. Treat translation health like test coverage: compute it on every push, and fail the build when it regresses. Here is how to wire that up in ten minutes.

The shape of the gate

A translation gate needs to answer one question per push: did any language get worse? That breaks down into checks a machine does better than a reviewer: every key in the source locale exists in every target, every placeholder that appears in the source survives translation, every ICU plural still parses, no language regressed below your floor.

Option 1: one curl call

Polylens exposes the gate as a single endpoint. POST your locale files with a project token; a failing gate returns HTTP 422, so curl -f exits non zero and the build goes red like any failing test.

curl -f -X POST https://polylens.sh/api/ci/scan \
  -H "Authorization: Bearer $POLYLENS_TOKEN" \
  -H "Content-Type: application/json" \
  -d "$(node -e 'const fs=require("fs");console.log(JSON.stringify({files:{en:fs.readFileSync("locales/en.json","utf8"),de:fs.readFileSync("locales/de.json","utf8")},failOn:"error"}))')"

Option 2: the zero-dependency helper

For more than two files, the helper script reads a whole locales directory and handles the request for you. It has no npm dependencies, so there is nothing to install in CI.

curl -fsSO https://polylens.sh/polylens-ci.mjs
POLYLENS_TOKEN=plk_... node polylens-ci.mjs ./locales

Option 3: GitHub Actions

name: translations
on: [push, pull_request]
jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: curl -fsSO https://polylens.sh/polylens-ci.mjs
      - run: node polylens-ci.mjs ./locales
        env:
          POLYLENS_TOKEN: ${{ secrets.POLYLENS_TOKEN }}

Tuning the gate

Start permissive and tighten. A gate that fails on day one for 200 legacy issues gets deleted by Friday. FAIL_ON=error with NO_REGRESSION=true is the setting that sticks: it freezes the status quo and refuses to let it get worse.

Why not just a key-diff script?

Plenty of teams have a 40-line script that compares key sets. It catches missing keys and nothing else: not the {{count}} a translator renamed, not the ICU plural that lost its other branch, not the English sentence pasted into the French file, not the translation that silently went stale when you reworded the source. A maintained scan engine with history catches all of it, and gives the team a number to rally around instead of a wall of diff output.

Get a health score for your locale files in under a minute. Free for one project.

More from the blog: all posts