From: Jiang Xin Date: Sun, 15 Feb 2026 06:06:19 +0000 (+0800) Subject: l10n: docs: add translation instructions in AGENTS.md X-Git-Tag: v2.54.0~1^2~15 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=fc59ba0dfe134fbb51230d50f396f5efc9ba1721;p=thirdparty%2Fgit.git l10n: docs: add translation instructions in AGENTS.md 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 --- diff --git a/po/AGENTS.md b/po/AGENTS.md index f2b8fc5100..69de4dafdd 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,356 @@ 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 | 三路合并 +# ... +``` + +**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** (placeholder reordering with precision): + +```po +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 +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): + +```po +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: + +```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 po/XX.po -o po/out.po + +# Get full context of changes since a commit (commit vs working tree) +git-po-helper compare --since po/XX.po -o po/out.po + +# Get full context between two commits +git-po-helper compare -r .. 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 ` | Compare parent of commit with the commit | +| `--since ` | 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 ` 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 +424,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 " >&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 [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 " >&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