Instructions for AI Agents

This file gives specific instructions for AI agents that perform housekeeping tasks for Git l10n. Use of AI is optional; many successful l10n teams work well without it.

The section “Housekeeping tasks for localization workflows” documents the most commonly used housekeeping tasks:

  1. Generating or updating po/git.pot
  2. Updating po/XX.po
  3. Translating po/XX.po
  4. Reviewing translation quality

Background knowledge for localization workflows

Essential background for the workflows below; understand these concepts before performing any housekeeping tasks in this document.

Language code and notation (XX, ll, ll_CC)

XX is a placeholder for the language code: either ll (ISO 639) or ll_CC (e.g. de, zh_CN). It appears in the PO file header metadata (e.g. "Language: zh_CN\n") and is typically used to name the PO file: po/XX.po.

Header Entry

The header entry is the first entry in every po/XX.po. It has an empty msgid; translation metadata (project, language, plural rules, encoding, etc.) is stored in msgstr, as in this example:

msgid ""
msgstr ""
"Project-Id-Version: Git\n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

CRITICAL: Do not edit the header's msgstr while translating. It holds metadata only and must be left unchanged.

Glossary Section

PO files may have a glossary in comments before the header entry (first msgid ""), giving terminology guidelines (e.g.):

# Git glossary for Chinese translators
#
#   English                          |  Chinese
#   ---------------------------------+--------------------------------------
#   3-way merge                      |  三路合并
#   ...

IMPORTANT: Read and use the glossary when translating or reviewing. It is in # comments only. Leave that comment block unchanged.

PO entry structure (single-line and multi-line)

PO entries are msgid / msgstr pairs. Plural messages add msgid_plural and msgstr[n]. The msgid is the immutable source; msgstr is the target translation. Each side may be a single quoted string or a multi-line block. In the multi-line form the header line is often msgid "" / msgstr "", with the real text split across following quoted lines (concatenated by Gettext).

Single-line entries:

msgid "commit message"
msgstr "提交说明"

Multi-line entries:

msgid ""
"Line 1\n"
"Line 2"
msgstr ""
"行 1\n"
"行 2"

CRITICAL: Do not use grep '^msgstr ""' to find untranslated entries; multi-line msgstr blocks use the same opening line, so grep gives false positives. Use msgattrib (next section).

Locating untranslated, fuzzy, and obsolete entries

Use msgattrib to list untranslated, fuzzy, and obsolete entries. Task 3 (translating po/XX.po) uses these commands.

  • Untranslated: msgattrib --untranslated --no-obsolete po/XX.po
  • Fuzzy: msgattrib --only-fuzzy --no-obsolete po/XX.po
  • Obsolete (#~): msgattrib --obsolete --no-wrap po/XX.po

Translating fuzzy entries

Fuzzy entries need re-translation because the source text changed. The format differs by file type:

  • PO file: A #, fuzzy tag in the entry comments marks the entry as fuzzy.
  • JSON file: The entry has "fuzzy": true.

Translation principles: Re-translate the msgstr (and, for plural entries, msgstr[n]) into the target language. Do not modify msgid or msgid_plural. After translation, clear the fuzzy mark: in PO, remove the #, fuzzy tag from comments; in JSON, omit or set fuzzy to false.

Preserving Special Characters

Preserve escape sequences (\n, \", \\, \t), placeholders (%s, %d, etc.), and quotes exactly as in msgid. Only reorder placeholders with positional syntax when needed (see Placeholder Reordering below).

Placeholder Reordering

When reordering placeholders relative to msgid, use positional syntax (%n$) where n is the 1-based argument index, so each argument still binds to the right value. Preserve width and precision modifiers, and place %n$ before them (see examples below).

Example 1 (placeholder reordering with precision):

msgid "missing environment variable '%s' for configuration '%.*s'"
msgstr "配置 '%3$.*2$s' 缺少环境变量 '%1$s'"

%s → argument 1 → %1$s. %.*s needs precision (arg 2) and string (arg 3) → %3$.*2$s.

Example 2 (multi-line, four %s reordered):

msgid ""
"Path updated: %s renamed to %s in %s, inside a directory that was renamed in "
"%s; moving it to %s."
msgstr ""
"路径已更新:%1$s 在 %3$s 中被重命名为 %2$s,而其所在目录又在 %4$s 中被重命"
"名,因此将其移动到 %5$s。"

Original order 1,2,3,4,5; in translation 1,3,2,4,5. Each line must be a complete quoted string.

Example 3 (no placeholder reordering):

msgid "MIDX %s must be an ancestor of %s"
msgstr "MIDX %s 必须是 %s 的祖先"

Argument order is still 1,2 in translation, so %n$ is not needed. If no placeholder reordering occurs, you must not introduce %n$ syntax; keep the original non-positional placeholders (%s, %d, etc.).

Validating PO File Format

Check the PO file using the command below:

msgfmt --check -o /dev/null po/XX.po

Common validation errors include:

  • Unclosed quotes
  • Missing escape sequences
  • Invalid placeholder syntax
  • Malformed multi-line entries
  • Incorrect line breaks in multi-line strings

On failure, msgfmt prints the line number; fix the PO at that line.

Using git-po-helper

git-po-helper supports Git l10n with quality checking (git-l10n PR conventions) and AI-assisted translation (subcommands for automated workflows). Housekeeping tasks in this document use it when available; otherwise rely on gettext tools.

Splitting large PO files

When a PO file is too large for translation or review, use git-po-helper msg-select to split it by entry index.

  • Entry 0 is the header (included by default; use --no-header to omit).
  • Entries 1, 2, 3, … are content entries.
  • Range format: --range "1-50" (entries 1 through 50), --range "-50" (first 50 entries), --range "51-" (from entry 51 to end). Shortcuts: --head N (first N), --tail N (last N), --since N (from N to end).
  • Output format: PO by default; use --json for GETTEXT JSON. See the “GETTEXT JSON format” section (under git-po-helper) for details.
  • State filter: Use --translated, --untranslated, --fuzzy to filter by state (OR relationship). Use --no-obsolete to exclude obsolete entries; --with-obsolete to include (default). Use --only-same or --only-obsolete for a single state. Range applies to the filtered list.
# First 50 entries (header + entries 1–50)
git-po-helper msg-select --range "-50" po/in.po -o po/out.po

# Entries 51–100
git-po-helper msg-select --range "51-100" po/in.po -o po/out.po

# Entries 101 to end
git-po-helper msg-select --range "101-" po/in.po -o po/out.po

# Entries 1–50 without header (content only)
git-po-helper msg-select --range "1-50" --no-header po/in.po -o po/frag.po

# Output as JSON; select untranslated and fuzzy entries, exclude obsolete
git-po-helper msg-select --json --untranslated --fuzzy --no-obsolete po/in.po >po/filtered.json

Comparing PO files for translation and review

git-po-helper compare shows PO changes with full entry context (unlike git diff). Redirect output to a file: it is empty when there are no new or changed entries; otherwise it contains a valid PO header.

# Get full context of local changes (HEAD vs working tree)
git-po-helper compare po/XX.po -o po/out.po

# Get full context of changes in a specific commit (parent vs commit)
git-po-helper compare --commit <commit> po/XX.po -o po/out.po

# Get full context of changes since a commit (commit vs working tree)
git-po-helper compare --since <commit> po/XX.po -o po/out.po

# Get full context between two commits
git-po-helper compare -r <commit1>..<commit2> po/XX.po -o po/out.po

# Get full context of two worktree files
git-po-helper compare po/old.po po/new.po -o po/out.po

# Check msgid consistency (detect tampering); no output means target matches source
git-po-helper compare --msgid po/old.po po/new.po >po/out.po

Options summary

OptionMeaning
(none)Compare HEAD with working tree (local changes)
--commit <commit>Compare parent of commit with the commit
--since <commit>Compare commit with working tree
-r x..yCompare revision x with revision y
-r x..Compare revision x with working tree
-r xCompare parent of x with x

Concatenating multiple PO/JSON files

git-po-helper msg-cat merges PO, POT, or gettext JSON inputs into one stream. Duplicate msgid values keep the first occurrence in file order. Write with -o <file> or stdout (-o - or omit); --json selects JSON output, else PO.

# Convert JSON to PO (e.g. after translation)
git-po-helper msg-cat --unset-fuzzy -o po/out.po po/in.json

# Merge multiple PO files
git-po-helper msg-cat -o po/out.po po/in-1.po po/in-2.json

GETTEXT JSON format

The GETTEXT JSON format is an internal format defined by git-po-helper for convenient batch processing of translation and related tasks by AI models. git-po-helper msg-select, git-po-helper msg-cat, and git-po-helper compare read and write this format.

Top-level structure:

{
  "header_comment": "string",
  "header_meta": "string",
  "entries": [ /* array of entry objects */ ]
}
FieldDescription
header_commentLines above the first msgid "" (comments, glossary), directly concatenated.
header_metaEncoded msgstr of the header entry (Project-Id-Version, Plural-Forms, etc.).
entriesList of PO entries. Order matches source.

Entry object (each element of entries):

FieldTypeDescription
msgidstringSingular message ID. PO escapes encoded (e.g. \n\\n).
msgstr[]stringTranslation forms as a JSON array only. Details below.
msgid_pluralstringPlural form of msgid. Omit for non-plural.
comments[]stringComment lines (#, #., #:, #,, etc.).
fuzzyboolTrue if entry has fuzzy flag.
obsoleteboolTrue for #~ obsolete entries. Omit if false.

msgstr array (required shape):

  • Always a JSON array of strings, never a single string. One element = singular (PO msgstr / msgstr[0]); multiple elements = plural forms in order (msgstr[0], msgstr[1], …).
  • Omit the key or use an empty array when the entry is untranslated.

Example (single-line entry):

{
  "header_comment": "# Glossary:\\n# term1\\tTranslation 1\\n#\\n",
  "header_meta": "Project-Id-Version: git\\nContent-Type: text/plain; charset=UTF-8\\n",
  "entries": [
    {
      "msgid": "Hello",
      "msgstr": ["你好"],
      "comments": ["#. Comment for translator\\n", "#: src/file.c:10\\n"],
      "fuzzy": false
    }
  ]
}

Example (plural entry):

{
  "msgid": "One file",
  "msgid_plural": "%d files",
  "msgstr": ["一个文件", "%d 个文件"],
  "comments": ["#, c-format\\n"]
}

Example (fuzzy entry before translation):

{
  "msgid": "Old message",
  "msgstr": ["旧翻译。"],
  "comments": ["#, fuzzy\\n"],
  "fuzzy": true
}

Translation notes for GETTEXT JSON files:

  • Preserve structure: Keep header_comment, header_meta, msgid, msgid_plural unchanged.
  • Fuzzy entries: Entries extracted from fuzzy PO entries have "fuzzy": true. After translating, remove the fuzzy field or set it to false in the output JSON. The merge step uses --unset-fuzzy, which can also remove the fuzzy field.
  • Placeholders: Preserve %s, %d, etc. exactly; use %n$ when reordering (see “Placeholder Reordering” above).

Quality checklist

  • Accuracy: Faithful to original meaning; no omissions or distortions.
  • Fuzzy entries: Re-translate fully and clear the fuzzy flag (see “Translating fuzzy entries” above).
  • Terminology: Consistent with glossary (see “Glossary Section” above) or domain standards.
  • Grammar and fluency: Correct and natural in the target language.
  • Placeholders: Preserve variables (%s, {name}, $1) exactly; use positional parameters when reordering (see “Placeholder Reordering” above).
  • Special characters: Preserve escape sequences (\n, \", \\, \t), placeholders exactly as in msgid. See “Preserving Special Characters” above.
  • Plurals and gender: Correct forms and agreement.
  • Context fit: Suitable for UI space, tone, and use (e.g. error vs. tooltip).
  • Cultural appropriateness: No offensive or ambiguous content.
  • Consistency: Match prior translations of the same source.
  • Technical integrity: Do not translate code, paths, commands, brands, or proper nouns.
  • Readability: Clear, concise, and user-friendly.

Housekeeping tasks for localization workflows

For common housekeeping tasks, follow the steps in the matching subsection below.

Task 1: Generating or updating po/git.pot

When asked to generate or update po/git.pot (or the like):

  1. Directly execute the command make po/git.pot without checking if the file exists beforehand.

  2. Do not verify the generated file after execution. Simply run the command and consider the task complete.

Task 2: Updating po/XX.po

When asked to update po/XX.po (or the like):

  1. Directly execute the command make po-update PO_FILE=po/XX.po without reading or checking the file content beforehand.

  2. Do not verify, translate, or review the updated file after execution. Simply run the command and consider the task complete.

Task 3: Translating po/XX.po

To translate po/XX.po, use the steps below. The script uses gettext or git-po-helper depending on what is installed; JSON export (when available) supports batch translation rather than per-entry work.

Workflow loop: Steps 1→2→3→4→5→6→7 form a loop. After step 6 succeeds, always go to step 7, which returns to step 1. The only exit to step 8 is when step 2 finds po/l10n-pending.po empty. Do not skip step 7 or jump to step 8 after step 6.

  1. Extract entries to translate: Directly execute the script below—it is authoritative; do not reimplement. It generates po/l10n-pending.po with messages that need translation.

    l10n_extract_pending () {
        test $# -ge 1 || { echo "Usage: l10n_extract_pending <po-file>" >&2; return 1; }
        PO_FILE="$1"
        PENDING="po/l10n-pending.po"
        PENDING_FUZZY="${PENDING}.fuzzy"
        PENDING_REFER="${PENDING}.fuzzy.reference"
        PENDING_UNTRANS="${PENDING}.untranslated"
        rm -f "$PENDING"
    
        if command -v git-po-helper >/dev/null 2>&1
        then
            git-po-helper msg-select --untranslated --fuzzy --no-obsolete -o "$PENDING" "$PO_FILE"
        else
            msgattrib --untranslated --no-obsolete "$PO_FILE" >"${PENDING_UNTRANS}"
            msgattrib --only-fuzzy --no-obsolete --clear-fuzzy --empty "$PO_FILE" >"${PENDING_FUZZY}"
            msgattrib --only-fuzzy --no-obsolete "$PO_FILE" >"${PENDING_REFER}"
            msgcat --use-first "${PENDING_UNTRANS}" "${PENDING_FUZZY}" >"$PENDING"
            rm -f "${PENDING_UNTRANS}" "${PENDING_FUZZY}"
        fi
        if test -s "$PENDING"
        then
            msgfmt --stat -o /dev/null "$PENDING" || true
            echo "Pending file is not empty; there are still entries to translate."
        else
            echo "No entries need translation."
            return 1
        fi
    }
    # Run the extraction. Example: l10n_extract_pending po/zh_CN.po
    l10n_extract_pending po/XX.po
    
  2. Check generated file: If po/l10n-pending.po is empty or does not exist, translation is complete; go to step 8. Otherwise proceed to step 3.

  3. Prepare one batch for translation: Batching keeps each run small so the model can complete translation within limited context. BEFORE translating, directly execute the script below—it is authoritative; do not reimplement. Based on which file the script produces: if po/l10n-todo.json exists, go to step 4a; if po/l10n-todo.po exists, go to step 4b.

    l10n_one_batch () {
        test $# -ge 1 || { echo "Usage: l10n_one_batch <po-file> [min_batch_size]" >&2; return 1; }
        PO_FILE="$1"
        min_batch_size=${2:-100}
        PENDING="po/l10n-pending.po"
        TODO_JSON="po/l10n-todo.json"
        TODO_PO="po/l10n-todo.po"
        DONE_JSON="po/l10n-done.json"
        DONE_PO="po/l10n-done.po"
        rm -f "$TODO_JSON" "$TODO_PO" "$DONE_JSON" "$DONE_PO"
    
        ENTRY_COUNT=$(grep -c '^msgid ' "$PENDING" 2>/dev/null || echo 0)
        ENTRY_COUNT=$((ENTRY_COUNT > 0 ? ENTRY_COUNT - 1 : 0))
    
        if test "$ENTRY_COUNT" -gt $min_batch_size
        then
            if test "$ENTRY_COUNT" -gt $((min_batch_size * 8))
            then
                NUM=$((min_batch_size * 2))
            elif test "$ENTRY_COUNT" -gt $((min_batch_size * 4))
            then
                NUM=$((min_batch_size + min_batch_size / 2))
            else
                NUM=$min_batch_size
            fi
            BATCHING=1
        else
            NUM=$ENTRY_COUNT
            BATCHING=
        fi
    
        if command -v git-po-helper >/dev/null 2>&1
        then
            if test -n "$BATCHING"
            then
                git-po-helper msg-select --json --head "$NUM" -o "$TODO_JSON" "$PENDING"
                echo "Processing batch of $NUM entries (out of $ENTRY_COUNT remaining)"
            else
                git-po-helper msg-select --json -o "$TODO_JSON" "$PENDING"
                echo "Processing all $ENTRY_COUNT entries at once"
            fi
        else
            if test -n "$BATCHING"
            then
                awk -v num="$NUM" '/^msgid / && count++ > num {exit} 1' "$PENDING" |
                    tac | awk '/^$/ {found=1} found' | tac >"$TODO_PO"
                echo "Processing batch of $NUM entries (out of $ENTRY_COUNT remaining)"
            else
                cp "$PENDING" "$TODO_PO"
                echo "Processing all $ENTRY_COUNT entries at once"
            fi
        fi
    }
    # Prepare one batch; shrink 2nd arg when batches exceed agent capacity.
    l10n_one_batch po/XX.po 100
    

4a. Translate JSON batch (po/l10n-todo.jsonpo/l10n-done.json):

  • Task: Translate po/l10n-todo.json (input, GETTEXT JSON) into po/l10n-done.json (output, GETTEXT JSON). See the “GETTEXT JSON format” section above for format details and translation rules.
  • Reference glossary: Read the glossary from the batch file's header_comment (see “Glossary Section” above) and use it for consistent terminology.
  • When translating: Follow the “Quality checklist” above for correctness and quality. Handle escape sequences (\n, \", \\, \t), placeholders, and quotes correctly as in msgid. For JSON, correctly escape and unescape these sequences when reading and writing. Modify msgstr and msgstr[n] (for plural entries); clear the fuzzy flag (omit or set fuzzy to false). Do not modify msgid or msgid_plural.

4b. Translate PO batch (po/l10n-todo.popo/l10n-done.po):

  • Task: Translate po/l10n-todo.po (input, GETTEXT PO) into po/l10n-done.po (output, GETTEXT PO).
  • Reference glossary: Read the glossary from the pending file header (see “Glossary Section” above) and use it for consistent terminology.
  • When translating: Follow the “Quality checklist” above for correctness and quality. Preserve escape sequences (\n, \", \\, \t), placeholders, and quotes as in msgid. Modify msgstr and msgstr[n] (for plural entries); remove the #, fuzzy tag from comments when done. Do not modify msgid or msgid_plural.
  1. Validate po/l10n-done.po:

    Run the validation script below. If it fails, fix per the errors and notes, re-run until it succeeds.

    l10n_validate_done () {
        DONE_PO="po/l10n-done.po"
        DONE_JSON="po/l10n-done.json"
        PENDING="po/l10n-pending.po"
    
        if test -f "$DONE_JSON" && { ! test -f "$DONE_PO" || test "$DONE_JSON" -nt "$DONE_PO"; }
        then
            git-po-helper msg-cat --unset-fuzzy -o "$DONE_PO" "$DONE_JSON" || {
                echo "ERROR [JSON to PO conversion]: Fix $DONE_JSON and re-run." >&2
                return 1
            }
        fi
    
        # Check 1: msgid should not be modified
        MSGID_OUT=$(git-po-helper compare -q --msgid --assert-no-changes \
            "$PENDING" "$DONE_PO" 2>&1)
        MSGID_RC=$?
        if test $MSGID_RC -ne 0 || test -n "$MSGID_OUT"
        then
            echo "ERROR [msgid modified]: The following entries appeared after" >&2
            echo "translation because msgid was altered. Fix in $DONE_PO." >&2
            echo "$MSGID_OUT" >&2
            return 1
        fi
    
        # Check 2: PO format (see "Validating PO File Format" for error handling)
        MSGFMT_OUT=$(msgfmt --check -o /dev/null "$DONE_PO" 2>&1)
        MSGFMT_RC=$?
        if test $MSGFMT_RC -ne 0
        then
            echo "ERROR [PO format]: Fix errors in $DONE_PO." >&2
            echo "$MSGFMT_OUT" >&2
            return 1
        fi
    
        echo "Validation passed."
    }
    l10n_validate_done
    

    If the script fails, fix directly in po/l10n-done.po. Re-run l10n_validate_done until it succeeds. Editing po/l10n-done.json is not recommended because it adds an extra JSON-to-PO conversion step. Use the error message to decide:

    • [msgid modified]: The listed entries have altered msgid; restore them to match po/l10n-pending.po.
    • [PO format]: msgfmt reports line numbers; fix the errors in place. See “Validating PO File Format” for common issues.
  2. Merge translation results into po/XX.po: Run the script below. If it fails, fix the file the error names: [JSON to PO conversion]po/l10n-done.json; [msgcat merge]po/l10n-done.po. Re-run until it succeeds.

    l10n_merge_batch () {
        test $# -ge 1 || { echo "Usage: l10n_merge_batch <po-file>" >&2; return 1; }
        PO_FILE="$1"
        DONE_PO="po/l10n-done.po"
        DONE_JSON="po/l10n-done.json"
        MERGED="po/l10n-done.merged"
        PENDING="po/l10n-pending.po"
        PENDING_REFER="${PENDING}.fuzzy.reference"
        TODO_JSON="po/l10n-todo.json"
        TODO_PO="po/l10n-todo.po"
        if test -f "$DONE_JSON" && { ! test -f "$DONE_PO" || test "$DONE_JSON" -nt "$DONE_PO"; }
        then
            git-po-helper msg-cat --unset-fuzzy -o "$DONE_PO" "$DONE_JSON" || {
                echo "ERROR [JSON to PO conversion]: Fix $DONE_JSON and re-run." >&2
                return 1
            }
        fi
        msgcat --use-first "$DONE_PO" "$PO_FILE" >"$MERGED" || {
            echo "ERROR [msgcat merge]: Fix errors in $DONE_PO and re-run." >&2
            return 1
        }
        mv "$MERGED" "$PO_FILE"
        rm -f "$TODO_JSON" "$TODO_PO" "$DONE_JSON" "$DONE_PO" "$PENDING_REFER"
    }
    # Run the merge. Example: l10n_merge_batch po/zh_CN.po
    l10n_merge_batch po/XX.po
    
  3. Loop: MUST return to step 1 (Extract entries) and repeat the cycle. Do not skip this step or go to step 8. Step 8 (below) runs only when step 2 finds no more entries and redirects there.

  4. Only after loop exits: Run the command below to validate the PO file and display the report. The process ends here.

    msgfmt --check --stat -o /dev/null po/XX.po
    

Task 4: Review translation quality

Review may target the full po/XX.po, a specific commit, or changes since a commit. When asked to review, follow the steps below.

Workflow: Follow steps in order. Do NOT use git show, git diff, git format-patch, or similar to get changes—they break PO context; use only git-po-helper compare for extraction. Without git-po-helper, refuse the task. Steps 3→4→5→6→7 loop: after step 6, always go to step 7 (back to step 3). The only ways to step 8 are when step 4 finds po/review-todo.json missing or empty (no batch left to review), or when step 1 finds po/review-result.json already present.

  1. Check for existing review (resume support): Evaluate the following in order:

    • If po/review-input.po does not exist, proceed to step 2 (Extract entries) for a fresh start.
    • Else If po/review-result.json exists, go to step 8 (only after loop exits).
    • Else If po/review-done.json exists, go to step 6 (Rename result).
    • Else if po/review-todo.json exists, go to step 5 (Review the current batch).
    • Else go to step 3 (Prepare one batch).
  2. Extract entries: Run git-po-helper compare with the desired range and redirect the output to po/review-input.po. See “Comparing PO files for translation and review” under git-po-helper for options.

  3. Prepare one batch: Batching keeps each run small so the model can complete review within limited context. Directly execute the script below—it is authoritative; do not reimplement.

    review_one_batch () {
        min_batch_size=${1:-100}
        INPUT_PO="po/review-input.po"
        PENDING="po/review-pending.po"
        TODO="po/review-todo.json"
        DONE="po/review-done.json"
        BATCH_FILE="po/review-batch.txt"
    
        if test ! -f "$INPUT_PO"
        then
            rm -f "$TODO"
            echo >&2 "cannot find $INPUT_PO, nothing for review"
            return 1
        fi
        if test ! -f "$PENDING" || test "$INPUT_PO" -nt "$PENDING"
        then
            rm -f "$BATCH_FILE" "$TODO" "$DONE"
            rm -f po/review-result*.json
            cp "$INPUT_PO" "$PENDING"
        fi
    
        ENTRY_COUNT=$(grep -c '^msgid ' "$PENDING" 2>/dev/null || echo 0)
        ENTRY_COUNT=$((ENTRY_COUNT > 0 ? ENTRY_COUNT - 1 : 0))
        if test "$ENTRY_COUNT" -eq 0
        then
            rm -f "$TODO"
            echo >&2 "No entries left for review"
            return 1
        fi
    
        if test "$ENTRY_COUNT" -gt $min_batch_size
        then
            if test "$ENTRY_COUNT" -gt $((min_batch_size * 8))
            then
                NUM=$((min_batch_size * 2))
            elif test "$ENTRY_COUNT" -gt $((min_batch_size * 4))
            then
                NUM=$((min_batch_size + min_batch_size / 2))
            else
                NUM=$min_batch_size
            fi
        else
            NUM=$ENTRY_COUNT
        fi
    
        BATCH=$(cat "$BATCH_FILE" 2>/dev/null || echo 0)
        BATCH=$((BATCH + 1))
        echo "$BATCH" >"$BATCH_FILE"
    
        git-po-helper msg-select --json --head "$NUM" -o "$TODO" "$PENDING"
        git-po-helper msg-select --since "$((NUM + 1))" -o "${PENDING}.tmp" "$PENDING"
        mv "${PENDING}.tmp" "$PENDING"
        echo "Processing batch $BATCH ($NUM entries out of $ENTRY_COUNT)"
    }
    # The parameter controls batch size; reduce if the batch file is too large.
    review_one_batch 100
    
  4. Check todo file: If po/review-todo.json does not exist or is empty, review is complete; go to step 8 (only after loop exits). Otherwise proceed to step 5.

  5. Review the current batch: Review translations in po/review-todo.json and write findings to po/review-done.json as follows:

    • Use “Background knowledge for localization workflows” for PO/JSON structure, placeholders, and terminology.
    • If header_comment includes a glossary, follow it for consistency.
    • Do not review the header (header_comment, header_meta).
    • For every other entry, check the entry's msgstr array (translation forms) against msgid / msgid_plural using the “Quality checklist” above.
    • Write JSON per “Review result JSON format” below; use {"issues": []} when there are no issues. Always write po/review-done.json—it marks the batch complete.
  6. Rename result: Rename po/review-done.json to po/review-result-<N>.json, where N is the value in po/review-batch.txt (the batch just completed). Run the script below:

    review_rename_result () {
        TODO="po/review-todo.json"
        DONE="po/review-done.json"
        BATCH_FILE="po/review-batch.txt"
        if test -f "$DONE"
        then
            N=$(cat "$BATCH_FILE" 2>/dev/null) || { echo "ERROR: $BATCH_FILE not found." >&2; return 1; }
            mv "$DONE" "po/review-result-$N.json"
            echo "Renamed to po/review-result-$N.json"
        fi
        rm -f "$TODO"
    }
    review_rename_result
    
  7. Loop: MUST return to step 3 (Prepare one batch) and repeat the cycle. Do not skip this step or go to step 8. Step 8 is reached only when step 4 finds po/review-todo.json missing or empty.

  8. Only after loop exits: Directly execute the command below. It merges results, applies suggestions, and displays the report. The process ends here.

    git-po-helper agent-run review --report po
    

    Do not run cleanup or delete intermediate files. Keep them for inspection or resumption.

Review result JSON format:

The Review result JSON format defines the structure for translation review reports. For each entry with translation issues, create an issue object as follows:

  • Copy the original entry's msgid, optional msgid_plural, and optional msgstr array (original translation forms) into the issue object. Use the same shape as GETTEXT JSON: msgstr is always a JSON array when present (one element singular, multiple for plural).
  • Write a summary of all issues found for this entry in description.
  • Set score according to the severity of issues found for this entry, from 0 to 3 (0 = critical; 1 = major; 2 = minor; 3 = perfect, no issues). Lower score means more severe issues.
  • Place the suggested translation in suggest_msgstr as a JSON array: one string for singular, multiple strings for plural forms in order. This is required for git-po-helper to apply suggestions.
  • Include only entries with issues (score less than 3). When no issues are found in the batch, write {"issues": []}.

Example review result (with issues):

{
  "issues": [
    {
      "msgid": "commit",
      "msgstr": ["委托"],
      "score": 0,
      "description": "Terminology error: 'commit' should be translated as '提交'",
      "suggest_msgstr": ["提交"]
    },
    {
      "msgid": "repository",
      "msgid_plural": "repositories",
      "msgstr": ["版本库", "版本库"],
      "score": 2,
      "description": "Consistency issue: suggest using '仓库' consistently",
      "suggest_msgstr": ["仓库", "仓库"]
    }
  ]
}

Field descriptions for each issue object (element of the issues array):

  • msgid (and optional msgid_plural for plural entries): Original source text.
  • msgstr (optional): JSON array of original translation forms (same meaning as in GETTEXT JSON entries).
  • suggest_msgstr: JSON array of suggested translation forms; must be an array (e.g. ["提交"] for singular). Plural entries use multiple elements in order.
  • score: 0–3 (0 = critical; 1 = major; 2 = minor; 3 = perfect, no issues).
  • description: Brief summary of the issue.

Human translators remain in control

Git translation is human-driven; language team leaders and contributors are responsible for maintaining translation quality and consistency.

AI-generated output should always be treated as drafts that must be reviewed and approved by someone who understands both the technical context and the target language. The best results come from combining AI efficiency with human judgment, cultural insight, and community engagement.