git/list[1] front-page[2] threads[3] people[4] search[5] about
wed 2026-10-07 16:55 UTC

[PATCH v3 4/5] docs(l10n): add AI agent instructions for translating PO files

From
Jiang Xin <worldhello.net@gmail.com>
Date
Mar 14, 2026, 14:38 UTC
Message-ID
<6c61a8ca25d5c7b848714f0ae28cdde7cd76fce9.1773497547.git.worldhello.net@gmail.com>
In-Reply-To
<cover.1773497547.git.worldhello.net@gmail.com>

Add a new "Translating po/XX.po" section to po/AGENTS.md with detailed workflow and procedures for AI agents to translate language-specific PO files. Users can invoke AI-assisted translation in coding tools with a prompt such as:

    "Translate the po/XX.po file by referring to @po/AGENTS.md"

Translation results serve as drafts; human contributors must review and approve before submission.

To address the low translation efficiency of some LLMs, batch translation replaces entry-by-entry translation. git-po-helper implements a gettext JSON format for translation files, replacing PO format during translation to enable batch processing.

Evaluation with the Qwen model:
    git-po-helper agent-run --agent=qwen translate po/zh_CN.po
Test translation (127 entries, 50 per batch):
    Initial state:  5998 translated, 91 fuzzy, 36 untranslated
    Final state:    6125 translated, 0 fuzzy, 0 untranslated
    Successfully translated: 127 entries (91 fuzzy + 36 untranslated)
    Success rate: 100%
Benchmark results (3-run average):
AI agent using gettext tools:
    | Metric           | Value                          |
    |------------------|--------------------------------|
    | Avg. Num turns   | 86 (176, 44, 40)               |
    | Avg. Exec. Time  | 20m44s (39m56s, 14m38s, 7m38s) |
    | Successful runs  | 3/3                            |
AI agent using git-po-helper (JSON batch flow):
    | Metric           | Value                          |
    |------------------|--------------------------------|
    | Avg. Num turns   | 56 (68, 39, 63)                |
    | Avg. Exec. Time  | 19m8s (28m55s, 9m1s, 19m28s)   |
    | Successful runs  | 3/3                            |

The git-po-helper flow reduces the number of turns (86 → 56) with similar execution time; the bottleneck appears to be LLM processing rather than network interaction.

Signed-off-by: Jiang Xin <worldhello.net@gmail.com>
---
 po/AGENTS.md | 596 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 595 insertions(+), 1 deletion(-)
diff --git a/po/AGENTS.md b/po/AGENTS.md
index f2b8fc5100..65017624f7 100644
--- a/po/AGENTS.md
+++ b/po/AGENTS.md
@@ -5,7 +5,11 @@ 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.
+most commonly used housekeeping tasks:
+
+1. Generating or updating po/git.pot
+2. Updating po/XX.po
+3. Translating po/XX.po
 
 
 ## Background knowledge for localization workflows
@@ -42,6 +46,351 @@ msgstr ""
 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.):
+
+```po
+# Git glossary for Chinese translators
+#
+#   English                          |  Chinese
+#   ---------------------------------+--------------------------------------
+#   3-way merge                      |  三路合并
+#   branch                           |  分支
+#   ...
+```
+
+**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**:
+
+```po
+msgid "commit message"
+msgstr "提交说明"
+```
+
+**Multi-line entries**:
+
+```po
+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** (precision):
+
+```po
+#, c-format
+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):
+
+```po
+#, c-format
+msgid ""
+"the 'submodule.%s.gitdir' config does not exist for module '%s'. Please "
+"ensure it is set, for example by running something like: 'git config "
+"submodule.%s.gitdir .git/modules/%s'. For details see the "
+"extensions.submodulePathConfig documentation."
+msgstr ""
+"模块 '%2$s' 的 'submodule.%1$s.gitdir' 配置不存在。请确保已设置,例如运行类"
+"似:'git config submodule.%3$s.gitdir .git/modules/%4$s'。详细信息请参见 "
+"extensions.submodulePathConfig 文档。"
+```
+
+Original order 1,2,3,4; in translation 2,1,3,4. Each line must be a complete
+quoted string.
+
+
+### Validating PO File Format
+
+Check the PO file using the command below:
+
+```shell
+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](https://github.com/git-l10n/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.
+
+```shell
+# 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.
+
+```shell
+# 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**
+
+| Option              | Meaning                                        |
+|---------------------|------------------------------------------------|
+| (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..y`           | Compare revision x with revision y             |
+| `-r x..`            | Compare revision x with working tree           |
+| `-r x`              | Compare 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.
+
+```shell
+# 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**:
+
+```json
+{
+  "header_comment": "string",
+  "header_meta": "string",
+  "entries": [ /* array of entry objects */ ]
+}
+```
+
+| Field            | Description                                                                    |
+|------------------|--------------------------------------------------------------------------------|
+| `header_comment` | Lines above the first `msgid ""` (comments, glossary), directly concatenated.  |
+| `header_meta`    | Encoded `msgstr` of the header entry (Project-Id-Version, Plural-Forms, etc.). |
+| `entries`        | List of PO entries. Order matches source.                                      |
+
+**Entry object** (each element of `entries`):
+
+| Field           | Type     | Description                                                  |
+|-----------------|----------|--------------------------------------------------------------|
+| `msgid`         | string   | Singular message ID. PO escapes encoded (e.g. `\n` → `\\n`). |
+| `msgstr`        | []string | Translation forms as a **JSON array only**. Details below.   |
+| `msgid_plural`  | string   | Plural form of msgid. Omit for non-plural.                   |
+| `comments`      | []string | Comment lines (`#`, `#.`, `#:`, `#,`, etc.).                 |
+| `fuzzy`         | bool     | True if entry has fuzzy flag.                                |
+| `obsolete`      | bool     | True 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)**:
+
+```json
+{
+  "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)**:
+
+```json
+{
+  "msgid": "One file",
+  "msgid_plural": "%d files",
+  "msgstr": ["一个文件", "%d 个文件"],
+  "comments": ["#, c-format\\n"]
+}
+```
+
+**Example (fuzzy entry before translation)**:
+
+```json
+{
+  "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
@@ -70,6 +419,251 @@ When asked to update `po/XX.po` (or the like):
    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.
+
+   ```shell
+   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.
+
+   ```shell
+   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.json` → `po/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.po` → `po/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`.
+
+5. **Validate `po/l10n-done.po`**:
+
+   Run the validation script below. If it fails, fix per the errors and notes,
+   re-run until it succeeds.
+
+   ```shell
+   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.
+
+
+6. **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.
+
+   ```shell
+   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
+   ```
+
+7. **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.
+
+8. **Only after loop exits**: Run the command below to validate the PO file and
+   display the report. The process ends here.
+
+   ```shell
+   msgfmt --check --stat -o /dev/null po/XX.po
+   ```
+
+
 ## Human translators remain in control
 
 Git translation is human-driven; language team leaders and contributors are
-- 
2.53.0.rc2.20.g532543fa46
Previous: Jiang XinNext: Jiang Xin
Message 30 of 45 in “[RFC] Introducing AI Agents to Git Localization”
  1. Jiang XinFeb 4, 2026
  2. Peter KreftingFeb 4, 2026
  3. Michal SuchánekFeb 4, 2026
  4. 依云Feb 4, 2026
  5. Jiang XinFeb 5, 2026
  6. brian m. carlsonFeb 5, 2026
  7. Jiang XinFeb 5, 2026
  8. Michal SuchánekFeb 5, 2026
  9. Jiang XinFeb 5, 2026
  10. Jiang XinFeb 5, 2026
  11. 1/2 l10n: add .gitattributes to simplify location filteringJiang Xin, Feb 5, 2026
  12. 2/2 l10n: README: document AI assistant guidelinesJiang Xin, Feb 5, 2026
  13. Michal SuchánekFeb 5, 2026
  14. Junio C HamanoFeb 5, 2026
  15. Junio C HamanoFeb 5, 2026
  16. Jiang XinFeb 6, 2026
  17. 0/5 docs(l10n): AI agent instructions and workflow improvementsJiang Xin, Mar 3, 2026
  18. 1/5 l10n: add .gitattributes to simplify location filteringJiang Xin, Mar 3, 2026
  19. 2/5 docs(l10n): add AGENTS.md with optimized update-pot instructionsJiang Xin, Mar 3, 2026
  20. 3/5 docs(l10n): add AI agent instructions for updating po/XX.po filesJiang Xin, Mar 3, 2026
  21. 4/5 docs(l10n): add AI agent instructions for translating PO filesJiang Xin, Mar 3, 2026
  22. 5/5 docs(l10n): add AI agent instructions to review translationsJiang Xin, Mar 3, 2026
  23. Jiang XinMar 12, 2026
  24. Jiang XinMar 12, 2026
  25. Jiang XinMar 12, 2026
  26. 0/5 docs(l10n): AI agent instructions and workflow improvementsJiang Xin, Mar 14, 2026
  27. 1/5 l10n: add .gitattributes to simplify location filteringJiang Xin, Mar 14, 2026
  28. 2/5 docs(l10n): add AGENTS.md with optimized update-pot instructionsJiang Xin, Mar 14, 2026
  29. 3/5 docs(l10n): add AI agent instructions for updating po/XX.po filesJiang Xin, Mar 14, 2026
  30. 4/5 docs(l10n): add AI agent instructions for translating PO filesJiang Xin, Mar 14, 2026
  31. 5/5 docs(l10n): add AI agent instructions to review translationsJiang Xin, Mar 14, 2026
  32. Johannes SixtMar 15, 2026
  33. Junio C HamanoMar 15, 2026
  34. Jiang XinMar 16, 2026
  35. Jiang XinMar 16, 2026
  36. Johannes SixtMar 16, 2026
  37. 0/5 docs(l10n): AI agent instructions and workflow improvementsJiang Xin, Mar 16, 2026
  38. 1/5 l10n: add .gitattributes to simplify location filteringJiang Xin, Mar 16, 2026
  39. 2/5 docs(l10n): add AGENTS.md with optimized update-pot instructionsJiang Xin, Mar 16, 2026
  40. 3/5 docs(l10n): add AI agent instructions for updating po/XX.po filesJiang Xin, Mar 16, 2026
  41. 4/5 docs(l10n): add AI agent instructions for translating PO filesJiang Xin, Mar 16, 2026
  42. 5/5 docs(l10n): add AI agent instructions to review translationsJiang Xin, Mar 16, 2026
  43. Jiang XinMar 31, 2026
  44. Junio C HamanoMar 31, 2026
  45. Jiang XinMar 31, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.