| # 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: | |
| ```po | |
| 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.): | |
| ```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 `git-po-helper msg-select` with `--untranslated` / `--fuzzy` | |
| (see Task 3), or `msgattrib` (e.g. `--untranslated`, `--only-fuzzy`, | |
| `--obsolete`). | |
| ### 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). | |
| ### Preserving Quotation Marks | |
| Some languages use language-specific UTF-8 quotation marks (curly/smart | |
| quotes) rather than ASCII straight quotes. **Always preserve these | |
| characters exactly as they appear in the source.** Do **not** convert them | |
| to ASCII straight quotes. | |
| **Protected quotation marks** (non-exhaustive list): | |
| | Character | Unicode | Name | Languages | | |
| |-----------|---------|------|-----------| | |
| | „ | U+201E | DOUBLE LOW-9 QUOTATION MARK | Bulgarian, German, etc. | | |
| | " | U+201C | LEFT DOUBLE QUOTATION MARK | Bulgarian, etc. | | |
| | " | U+201D | RIGHT DOUBLE QUOTATION MARK | English, German, etc. | | |
| | ' | U+2018 | LEFT SINGLE QUOTATION MARK | English, etc. | | |
| | ' | U+2019 | RIGHT SINGLE QUOTATION MARK | English, etc. | | |
| | « | U+00AB | LEFT-POINTING DOUBLE ANGLE QUOTATION MARK | French, Russian, etc. | | |
| | » | U+00BB | RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK | French, Russian, etc. | | |
| | ‹ | U+2039 | SINGLE LEFT-POINTING ANGLE QUOTATION MARK | French, etc. | | |
| | › | U+203A | SINGLE RIGHT-POINTING ANGLE QUOTATION MARK | French, etc. | | |
| **Why this matters in PO files**: In PO file format, the ASCII straight | |
| double quote `"` (U+0022) is the **string delimiter**. If a translation | |
| contains a curly quote that is incorrectly converted to `"` (U+0022), | |
| the PO parser will interpret it as the end of the string, causing: | |
| 1. **String truncation**: The `msgstr` value is cut short at the | |
| spurious quote character. | |
| 2. **Syntax errors**: `msgfmt --check` fails with parse errors at | |
| the line where the string was prematurely terminated. | |
| 3. **Data loss**: Content after the accidental quote delimiter is | |
| misinterpreted or lost. | |
| **Rules**: | |
| - **Never** replace language-specific quotation marks with ASCII | |
| straight quotes `"` (U+0022) or `'` (U+0027). | |
| - Apply this rule when translating PO files, PO multi-line strings, | |
| and GETTEXT JSON `msgstr` array values. | |
| - Apply this rule when generating suggested translations | |
| (`suggest_msgstr`) during review. | |
| - If the source `msgid` uses ASCII straight quotes, preserve them | |
| as-is in the translation unless the target language convention | |
| requires different quotation marks. | |
| ### 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). Task 3 (translating) and Task 4 | |
| (reviewing) **require** `git-po-helper` **>= 0.9.1**. Task 1 and Task 2 use | |
| `make` / gettext tooling instead. | |
| **Version check**: Before Task 3 or Task 4, run: | |
| ```shell | |
| git-po-helper version --ge 0.9.1 | |
| ``` | |
| If the command is missing or the exit status is non-zero, print the error (use | |
| the command's own output when available) and **refuse** the task; do not | |
| continue. | |
| #### 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 --json --commit <commit> po/XX.po -o po/out.json | |
| # Get full context of changes since a commit (commit vs working tree) | |
| git-po-helper compare --json --since <commit> po/XX.po -o po/out.json | |
| # 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`. Preserve language-specific quotation | |
| marks (curly/smart quotes like „, ", ", ', ') — do not convert them to | |
| ASCII straight quotes. See "Preserving Special Characters" and | |
| "Preserving Quotation Marks" 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. **Require** `git-po-helper` (see | |
| "Using git-po-helper" for the version check); without a passing check, refuse | |
| the task. Batches are GETTEXT JSON for translation. | |
| **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.json` 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.json` 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.json" | |
| rm -f "$PENDING" | |
| git-po-helper msg-select --json --untranslated --fuzzy --no-obsolete -o "$PENDING" "$PO_FILE" | |
| ENTRY_COUNT=$(git-po-helper stat -c "$PENDING" 2>/dev/null || true) | |
| ENTRY_COUNT=${ENTRY_COUNT:-0} | |
| if test "$ENTRY_COUNT" -gt 0 | |
| then | |
| git-po-helper stat "$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.json` has no content entries | |
| (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. | |
| It writes `po/l10n-todo.json`; then go to step 4. | |
| ```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.json" | |
| TODO_JSON="po/l10n-todo.json" | |
| DONE_JSON="po/l10n-done.json" | |
| DONE_PO_TMP="po/l10n-done.po.tmp" | |
| rm -f "$TODO_JSON" "$DONE_JSON" "$DONE_PO_TMP" | |
| ENTRY_COUNT=$(git-po-helper stat -c "$PENDING" 2>/dev/null || true) | |
| ENTRY_COUNT=${ENTRY_COUNT:-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 | |
| 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 | |
| } | |
| # Prepare one batch; shrink 2nd arg when batches exceed agent capacity. | |
| l10n_one_batch po/XX.po 100 | |
| ``` | |
| 4. **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`. | |
| 5. **Validate `po/l10n-done.po.tmp`**: | |
| 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_TMP="po/l10n-done.po.tmp" | |
| DONE_JSON="po/l10n-done.json" | |
| PENDING="po/l10n-pending.json" | |
| if test -f "$DONE_JSON" && { ! test -f "$DONE_PO_TMP" || test "$DONE_JSON" -nt "$DONE_PO_TMP"; } | |
| then | |
| git-po-helper msg-cat --unset-fuzzy -o "$DONE_PO_TMP" "$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_TMP" 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_TMP." >&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_TMP" 2>&1) | |
| MSGFMT_RC=$? | |
| if test $MSGFMT_RC -ne 0 | |
| then | |
| echo "ERROR [PO format]: Fix errors in $DONE_PO_TMP." >&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.tmp`**. 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.json`. | |
| - **`[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.tmp`. 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_TMP="po/l10n-done.po.tmp" | |
| DONE_JSON="po/l10n-done.json" | |
| MERGED="po/l10n-done.merged" | |
| TODO_JSON="po/l10n-todo.json" | |
| if test -f "$DONE_JSON" && { ! test -f "$DONE_PO_TMP" || test "$DONE_JSON" -nt "$DONE_PO_TMP"; } | |
| then | |
| git-po-helper msg-cat --unset-fuzzy -o "$DONE_PO_TMP" "$DONE_JSON" || { | |
| echo "ERROR [JSON to PO conversion]: Fix $DONE_JSON and re-run." >&2 | |
| return 1 | |
| } | |
| fi | |
| msgcat --use-first "$DONE_PO_TMP" "$PO_FILE" >"$MERGED" || { | |
| echo "ERROR [msgcat merge]: Fix errors in $DONE_PO_TMP and re-run." >&2 | |
| return 1 | |
| } | |
| mv "$MERGED" "$PO_FILE" | |
| rm -f "$TODO_JSON" "$DONE_JSON" "$DONE_PO_TMP" | |
| } | |
| # 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 | |
| ``` | |
| ### 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. **Require** `git-po-helper` (see | |
| "Using git-po-helper" for the version check); without a passing check, 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.json` 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 --json` with the desired range | |
| and write the output to `po/review-input.json` (use `-o`). 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. | |
| ```shell | |
| review_one_batch () { | |
| min_batch_size=${1:-100} | |
| INPUT_JSON="po/review-input.json" | |
| PENDING="po/review-pending.json" | |
| TODO="po/review-todo.json" | |
| DONE="po/review-done.json" | |
| BATCH_FILE="po/review-batch.txt" | |
| if test ! -f "$INPUT_JSON" | |
| then | |
| rm -f "$TODO" | |
| echo >&2 "cannot find $INPUT_JSON, nothing for review" | |
| return 1 | |
| fi | |
| if test ! -f "$PENDING" || test "$INPUT_JSON" -nt "$PENDING" | |
| then | |
| # zsh aborts on unmatched globs; allow empty expansion here. | |
| if test -n "$ZSH_VERSION" | |
| then | |
| setopt local_options null_glob | |
| fi | |
| rm -f po/review-result*.json | |
| rm -f "$BATCH_FILE" "$TODO" "$DONE" | |
| cp "$INPUT_JSON" "$PENDING" | |
| fi | |
| ENTRY_COUNT=$(git-po-helper stat -c "$PENDING" 2>/dev/null || true) | |
| ENTRY_COUNT=${ENTRY_COUNT:-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 || true) | |
| BATCH=${BATCH:-0} | |
| BATCH=$((BATCH + 1)) | |
| echo "$BATCH" >"$BATCH_FILE" | |
| git-po-helper msg-select --json --head "$NUM" -o "$TODO" "$PENDING" | |
| git-po-helper msg-select --json --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: | |
| ```shell | |
| 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. | |
| ```shell | |
| 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): | |
| ```json | |
| { | |
| "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. |