AI & review

Somebody has to say yes to a sentence nobody here can read

Every translation workflow is an answer to one problem: the person accountable for the release usually cannot judge the string. Most of them answer it with a status field and hope. Here is what the states have to be, why “rejected” is not one of them, and what has to be true for a pipeline to trust the answer.

Published Updated

Approval that means nothing

Code review works because the reviewer can read the code. Translation review has the one problem code review never has: the person accountable for the release is often the person least able to judge the string. A workflow that asks them to approve it anyway produces a queue of green ticks and no information.

That is not merely useless. It converts “nobody has checked this” into “this has been checked”, which is worse than the state it started in.

Three ways it happens, and none of them is anybody cutting corners:

  • The reviewer cannot read it. Somebody has to approve the locale, so whoever owns the release does. The tick records that a form was submitted.
  • The feedback leaves the string. Notes live in chat, a document, an email thread. The translator gets the note and the string separately, and the next person to open that key gets neither.
  • The check is not on the path. If nothing consults the review before the strings ship, it is documentation of an intention. The deploy happens whether the queue is empty or not.

Rejected is an event

The obvious design has four states: draft, translated, reviewed, rejected. It is the wrong one, and the reason is worth a paragraph because almost every tool in this space gets it wrong in the same direction.

“Rejected” is not a state a string can rest in. It is something that happened to it. Give it a status of its own and you now have to answer: what does a rejected string that somebody then edits become? What does a downloaded file do with one? Does it count as translated for a progress number? Every answer is arbitrary, and each one is a rule somebody has to learn.

Three states answer all of it. A rejection puts the value back where an unfinished value belongs — draft — and attaches the reason. The state says what is true now; the comment says what happened. There is no fourth case to invent a rule for.

A filter for “rejected” is still worth having, and it is a question rather than a status: which drafts carry a reason?

The cost of a silent send-back

Rejecting without saying why sends the string back to somebody who now has to guess, and guessing produces a second round. Two rounds on one string is the whole budget for a feature’s worth of copy.

So the reason is not optional, and it belongs on the key rather than in a notification. Anyone who opens that string later — a different translator, the same one in three months, whoever is auditing why a locale stalled — reads the reason next to the text it is about.

The reverse is worth having too and should stay optional: an approval with a note about a phrasing choice, for the next person, that does not hold up the approval.

Where Mergua fits

Three statuses, and a rejection is not one

Mergua has exactly the three above — draft, translated, reviewed — and a rejection is a draft carrying its reason. Rejecting requires a comment, clears the reviewer and the review date, and posts the reason on the key; the translator’s next edit is what makes it translated again and ready for another look. The editor has a Rejected filter, and what it finds is drafts with a reason.

one value, one round trip
checkout.pay.confirm  ·  de

Marie   translated              →  TRANSLATED
Anna    rejected                →  DRAFT
        “Zu formell — bitte du statt Sie.”
Marie   edited                  →  TRANSLATED
Anna    approved                →  REVIEWED
        reviewer and date recorded
what each move persists
reject
  status        → DRAFT
  reviewer      → cleared
  reviewed at   → cleared
  comment       → required, posted on the key
  history       → “rejected: <reason>”

approve
  status        → REVIEWED
  reviewer      → you
  reviewed at   → now
  comment       → optional
  history       → “approved”

How anybody finds out

A reason on the key is only worth writing if the person it is for reads it, and the section above says why it is not a notification: the note has to survive being read in three months, next to the text it is about. But that argument is about where the reason lives, not about how somebody learns it exists — and for a while the answer to the second question was that they opened each project in turn and looked.

So there is one page, across every project: branches carrying work nobody has approved if you review, and the values you wrote that came back if you translate. Longest wait at the top of each, grouped by project, and a click opens the editor on the branch — on the cell the comment is about, when it is a value of yours that came back.

What it does not have is a bell, and that is a decision rather than a gap. A bell promises an unread count, and an unread count needs somewhere to record that you have seen something. For a review and for a send-back Mergua records no such thing, deliberately: the number beside the link is worked out fresh every time from the same count that decides whether a branch can be merged, so it says how much is waiting on you and never how much is new. Neither needs marking as read, because neither ever claimed to be unread — and a badge that never clears is worse than no badge. Rewriting the value is what makes the row disappear, which is the same rule as the rejection itself: a state that follows from the work, not a flag somebody has to remember to clear.

The one thing on that page with a read state is a comment that named you, and it has one for the opposite reason. Everything else stops waiting on its own once the work is done; a question does not, so without a read state it would stand in the list forever. It marks itself the moment you have the thread on screen, which is still not a flag anybody has to remember to clear.

Email is the exception to the rest of it, and it is deliberately narrow: a branch sent for review, and the mentions you have not read. Both are somebody addressing you rather than work quietly arriving, both are a shortcut back to the page rather than a second place to keep track, and one switch turns both off without touching account email. Apart from those two, nothing else is mailed — a translation landing, a value approved, a branch merged all wait to be found, because a mail per change is a mail nobody reads by the second week.

Who the mail is addressed to, and how to stop it

A request goes to everybody in the organization who holds a role that could answer it, and to nobody else. That is not a recipient list somebody maintains: the authority to approve is what makes a person a recipient, so a maintainer who joined this morning is asked and a translator who has been there a year is not. The person who pressed the button drops out of the set, however senior they are — a mail describing what you have just done is the clearest possible signal that a channel is noise. Each one names the project, the branch and how many changes are waiting, and it opens the queue rather than one key, because with a branch full of changes any single link is the wrong one.

A mention works the other way round, because it has a window. Being named collects for five minutes, and the mail that goes at the end of it carries every mention of yours in that organization still unread — so four in one editing session is one mail, and the window opens with the first of them rather than being pushed back by the next. A mention you have already read in the app before it closes stays out of the mail altogether, which is the whole reason the five minutes exist: the page gets first refusal.

And if nobody wants either of them, one switch in your account settings turns both off. The other door is the unsubscribe link in the foot of the mail itself, which does not ask you to sign in first — the token in the link is the whole of the authentication, it can reach nothing else about the account, and a mail client that offers its own one-click unsubscribe is sending the same request. It works in both directions, so a mis-click is not a trip through a login screen. Account mail is untouched: verifying an address, resetting a password and an invitation to an organization all arrive whatever the switch says.

Nothing about the work changes when it is off, and that is the test of whether the mail was ever load-bearing. Every request still stands in the queue, longest wait first, with the same count beside it; somebody who has switched it off is simply never a recipient, rather than a recipient the send skips over. That is the difference between a shortcut to a pull channel and a push channel with a pull page attached — the second one is the arrangement where turning the mail off means work quietly going missing, and it is the reason a team can make this choice per person without agreeing on it first.

Who is allowed to say yes

Four roles, and the split is not about seniority. It is about which side of the review somebody is on: filling a translation in and signing it off are different permissions, so an invited translator cannot mark their own work finished.

The last column is the one that makes the rest of them usable. A reviewer needs to approve, reject and merge; they do not need the invoice, the member list or the project settings. Bundling the two meant a team could have too few reviewers or too many owners, and a review nobody is available to do is the approval that means nothing this article opened on. Maintainer is the rung that takes the first half without the second.

RoleTranslatesApproves & rejectsMerges a branchRuns the organization
OWNERRuns the projectYesYesYesYes — billing, members, project settings
MAINTAINERRuns the translationsYesYesYesNo
TRANSLATORInvited to writeYes — and can send a locale back to draftNoNoNo
VIEWERInvited to lookNoNoNoNo

Two bulk actions exist for the same reason a queue does: approve everything that is translated, or send a whole locale back to draft. The second one is the only move in this section a Translator can make alone. Review can also be switched off per project — Require approval before merge, on by default. With it on, a branch cannot merge while any of its operations is unreviewed; with it off the statuses still move and still show, they just stop being a gate.

Team & invitations covers how somebody gets a role, and the manual goes field by field through the filters, the bulk actions and the comment thread.

The part a pipeline can read

A review only a human can see is a review your deploy walks past. On a branch the status sits on the change rather than on the string, which is what makes the whole branch answerable in one question — and that question is one a CI job can ask before it writes a single file. Approving an added key approves the value that arrived with it, since two clicks for one decision would be two chances to approve half of it.

a job that waits for the review
$ curl -sf https://api.mergua.com/v1/mergua.sh \
    | sh -s -- pull --mode reviewed

=== Readiness Check ===
Branch 'feature/checkout' not ready: not fully
reviewed (1/2 locales ready).
  pending: ADD_KEY checkout.pay.confirm
Skipping translation pull. Pass --mode always to
pull anyway.
the same job, once it is signed off
$ curl -sf https://api.mergua.com/v1/mergua.sh \
    | sh -s -- pull --mode reviewed

=== Readiness Check ===
Branch 'feature/checkout' is ready (2/2 locales
reviewed, no conflicts).

  → src/assets/i18n/de.json
  → src/assets/i18n/fr.json

None of that is a convention the job has to be trusted to follow. The readiness answer is computed from the operations on the branch, so a job configured to wait cannot be talked into pulling half-reviewed strings — and one that does not want to wait passes the flag that says so, in the open, in the file everybody reads. The CLI reference has the rest of the flags.

Keep reading

Try it on one branch.

Upload the locale files you already have and see the whole loop — branch, translate, review, sync — on the free tier.

Create your account