| 1 | # Instructions for AI Agents |
| 2 | |
| 3 | This file gives specific instructions for AI agents that perform |
| 4 | housekeeping tasks for Git l10n. Use of AI is optional; many successful |
| 5 | l10n teams work well without it. |
| 6 | |
| 7 | The section "Housekeeping tasks for localization workflows" documents the |
| 8 | most commonly used housekeeping tasks: |
| 9 | |
| 10 | 1. Generating or updating po/git.pot |
| 11 | 2. Updating po/XX.po |
| 12 | 3. Translating po/XX.po |
| 13 | 4. Reviewing translation quality |
| 14 | |
| 15 | |
| 16 | ## Background knowledge for localization workflows |
| 17 | |
| 18 | Essential background for the workflows below; understand these concepts before |
| 19 | performing any housekeeping tasks in this document. |
| 20 | |
| 21 | ### Language code and notation (XX, ll, ll\_CC) |
| 22 | |
| 23 | **XX** is a placeholder for the language code: either `ll` (ISO 639) or |
| 24 | `ll_CC` (e.g. `de`, `zh_CN`). It appears in the PO file header metadata |
| 25 | (e.g. `"Language: zh_CN\n"`) and is typically used to name the PO file: |
| 26 | `po/XX.po`. |
| 27 | |
| 28 | |
| 29 | ### Header Entry |
| 30 | |
| 31 | The **header entry** is the first entry in every `po/XX.po`. It has an empty |
| 32 | `msgid`; translation metadata (project, language, plural rules, encoding, etc.) |
| 33 | is stored in `msgstr`, as in this example: |
| 34 | |
| 35 | ```po |
| 36 | msgid "" |
| 37 | msgstr "" |
| 38 | "Project-Id-Version: Git\n" |
| 39 | "Language: zh_CN\n" |
| 40 | "MIME-Version: 1.0\n" |
| 41 | "Content-Type: text/plain; charset=UTF-8\n" |
| 42 | "Content-Transfer-Encoding: 8bit\n" |
| 43 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" |
| 44 | ``` |
| 45 | |
| 46 | **CRITICAL**: Do not edit the header's `msgstr` while translating. It holds |
| 47 | metadata only and must be left unchanged. |
| 48 | |
| 49 | |
| 50 | ### Glossary Section |
| 51 | |
| 52 | PO files may have a glossary in comments before the header entry (first |
| 53 | `msgid ""`), giving terminology guidelines (e.g.): |
| 54 | |
| 55 | ```po |
| 56 | # Git glossary for Chinese translators |
| 57 | # |
| 58 | # English | Chinese |
| 59 | # ---------------------------------+-------------------------------------- |
| 60 | # 3-way merge | 三路合并 |
| 61 | # ... |
| 62 | ``` |
| 63 | |
| 64 | **IMPORTANT**: Read and use the glossary when translating or reviewing. It is |
| 65 | in `#` comments only. Leave that comment block unchanged. |
| 66 | |
| 67 | |
| 68 | ### PO entry structure (single-line and multi-line) |
| 69 | |
| 70 | PO entries are `msgid` / `msgstr` pairs. Plural messages add `msgid_plural` and |
| 71 | `msgstr[n]`. The `msgid` is the immutable source; `msgstr` is the target |
| 72 | translation. Each side may be a single quoted string or a multi-line block. |
| 73 | In the multi-line form the header line is often `msgid ""` / `msgstr ""`, with |
| 74 | the real text split across following quoted lines (concatenated by Gettext). |
| 75 | |
| 76 | **Single-line entries**: |
| 77 | |
| 78 | ```po |
| 79 | msgid "commit message" |
| 80 | msgstr "提交说明" |
| 81 | ``` |
| 82 | |
| 83 | **Multi-line entries**: |
| 84 | |
| 85 | ```po |
| 86 | msgid "" |
| 87 | "Line 1\n" |
| 88 | "Line 2" |
| 89 | msgstr "" |
| 90 | "行 1\n" |
| 91 | "行 2" |
| 92 | ``` |
| 93 | |
| 94 | **CRITICAL**: Do **not** use `grep '^msgstr ""'` to find untranslated entries; |
| 95 | multi-line `msgstr` blocks use the same opening line, so grep gives false |
| 96 | positives. Use `msgattrib` (next section). |
| 97 | |
| 98 | |
| 99 | ### Locating untranslated, fuzzy, and obsolete entries |
| 100 | |
| 101 | Use `msgattrib` to list untranslated, fuzzy, and obsolete entries. Task 3 |
| 102 | (translating `po/XX.po`) uses these commands. |
| 103 | |
| 104 | - **Untranslated**: `msgattrib --untranslated --no-obsolete po/XX.po` |
| 105 | - **Fuzzy**: `msgattrib --only-fuzzy --no-obsolete po/XX.po` |
| 106 | - **Obsolete** (`#~`): `msgattrib --obsolete --no-wrap po/XX.po` |
| 107 | |
| 108 | |
| 109 | ### Translating fuzzy entries |
| 110 | |
| 111 | Fuzzy entries need re-translation because the source text changed. The format |
| 112 | differs by file type: |
| 113 | |
| 114 | - **PO file**: A `#, fuzzy` tag in the entry comments marks the entry as fuzzy. |
| 115 | - **JSON file**: The entry has `"fuzzy": true`. |
| 116 | |
| 117 | **Translation principles**: Re-translate the `msgstr` (and, for plural entries, |
| 118 | `msgstr[n]`) into the target language. Do **not** modify `msgid` or |
| 119 | `msgid_plural`. After translation, **clear the fuzzy mark**: in PO, remove the |
| 120 | `#, fuzzy` tag from comments; in JSON, omit or set `fuzzy` to `false`. |
| 121 | |
| 122 | |
| 123 | ### Preserving Special Characters |
| 124 | |
| 125 | Preserve escape sequences (`\n`, `\"`, `\\`, `\t`), placeholders (`%s`, `%d`, |
| 126 | etc.), and quotes exactly as in `msgid`. Only reorder placeholders with |
| 127 | positional syntax when needed (see Placeholder Reordering below). |
| 128 | |
| 129 | |
| 130 | ### Placeholder Reordering |
| 131 | |
| 132 | When reordering placeholders relative to `msgid`, use positional syntax (`%n$`) |
| 133 | where *n* is the 1-based argument index, so each argument still binds to the |
| 134 | right value. Preserve width and precision modifiers, and place `%n$` before |
| 135 | them (see examples below). |
| 136 | |
| 137 | **Example 1** (placeholder reordering with precision): |
| 138 | |
| 139 | ```po |
| 140 | msgid "missing environment variable '%s' for configuration '%.*s'" |
| 141 | msgstr "配置 '%3$.*2$s' 缺少环境变量 '%1$s'" |
| 142 | ``` |
| 143 | |
| 144 | `%s` → argument 1 → `%1$s`. `%.*s` needs precision (arg 2) and string (arg 3) → |
| 145 | `%3$.*2$s`. |
| 146 | |
| 147 | **Example 2** (multi-line, four `%s` reordered): |
| 148 | |
| 149 | ```po |
| 150 | msgid "" |
| 151 | "Path updated: %s renamed to %s in %s, inside a directory that was renamed in " |
| 152 | "%s; moving it to %s." |
| 153 | msgstr "" |
| 154 | "路径已更新:%1$s 在 %3$s 中被重命名为 %2$s,而其所在目录又在 %4$s 中被重命" |
| 155 | "名,因此将其移动到 %5$s。" |
| 156 | ``` |
| 157 | |
| 158 | Original order 1,2,3,4,5; in translation 1,3,2,4,5. Each line must be a |
| 159 | complete quoted string. |
| 160 | |
| 161 | **Example 3** (no placeholder reordering): |
| 162 | |
| 163 | ```po |
| 164 | msgid "MIDX %s must be an ancestor of %s" |
| 165 | msgstr "MIDX %s 必须是 %s 的祖先" |
| 166 | ``` |
| 167 | |
| 168 | Argument order is still 1,2 in translation, so `%n$` is not needed. |
| 169 | If no placeholder reordering occurs, you **must not** introduce `%n$` |
| 170 | syntax; keep the original non-positional placeholders (`%s`, `%d`, etc.). |
| 171 | |
| 172 | |
| 173 | ### Validating PO File Format |
| 174 | |
| 175 | Check the PO file using the command below: |
| 176 | |
| 177 | ```shell |
| 178 | msgfmt --check -o /dev/null po/XX.po |
| 179 | ``` |
| 180 | |
| 181 | Common validation errors include: |
| 182 | - Unclosed quotes |
| 183 | - Missing escape sequences |
| 184 | - Invalid placeholder syntax |
| 185 | - Malformed multi-line entries |
| 186 | - Incorrect line breaks in multi-line strings |
| 187 | |
| 188 | On failure, `msgfmt` prints the line number; fix the PO at that line. |
| 189 | |
| 190 | |
| 191 | ### Using git-po-helper |
| 192 | |
| 193 | [git-po-helper](https://github.com/git-l10n/git-po-helper) supports Git l10n with |
| 194 | **quality checking** (git-l10n PR conventions) and **AI-assisted translation** |
| 195 | (subcommands for automated workflows). Housekeeping tasks in this document use |
| 196 | it when available; otherwise rely on gettext tools. |
| 197 | |
| 198 | |
| 199 | #### Splitting large PO files |
| 200 | |
| 201 | When a PO file is too large for translation or review, use `git-po-helper |
| 202 | msg-select` to split it by entry index. |
| 203 | |
| 204 | - **Entry 0** is the header (included by default; use `--no-header` to omit). |
| 205 | - **Entries 1, 2, 3, …** are content entries. |
| 206 | - **Range format**: `--range "1-50"` (entries 1 through 50), `--range "-50"` |
| 207 | (first 50 entries), `--range "51-"` (from entry 51 to end). Shortcuts: |
| 208 | `--head N` (first N), `--tail N` (last N), `--since N` (from N to end). |
| 209 | - **Output format**: PO by default; use `--json` for GETTEXT JSON. See the |
| 210 | "GETTEXT JSON format" section (under git-po-helper) for details. |
| 211 | - **State filter**: Use `--translated`, `--untranslated`, `--fuzzy` to filter |
| 212 | by state (OR relationship). Use `--no-obsolete` to exclude obsolete entries; |
| 213 | `--with-obsolete` to include (default). Use `--only-same` or `--only-obsolete` |
| 214 | for a single state. Range applies to the filtered list. |
| 215 | |
| 216 | ```shell |
| 217 | # First 50 entries (header + entries 1–50) |
| 218 | git-po-helper msg-select --range "-50" po/in.po -o po/out.po |
| 219 | |
| 220 | # Entries 51–100 |
| 221 | git-po-helper msg-select --range "51-100" po/in.po -o po/out.po |
| 222 | |
| 223 | # Entries 101 to end |
| 224 | git-po-helper msg-select --range "101-" po/in.po -o po/out.po |
| 225 | |
| 226 | # Entries 1–50 without header (content only) |
| 227 | git-po-helper msg-select --range "1-50" --no-header po/in.po -o po/frag.po |
| 228 | |
| 229 | # Output as JSON; select untranslated and fuzzy entries, exclude obsolete |
| 230 | git-po-helper msg-select --json --untranslated --fuzzy --no-obsolete po/in.po >po/filtered.json |
| 231 | ``` |
| 232 | |
| 233 | |
| 234 | #### Comparing PO files for translation and review |
| 235 | |
| 236 | `git-po-helper compare` shows PO changes with full entry context (unlike |
| 237 | `git diff`). Redirect output to a file: it is empty when there are no new or |
| 238 | changed entries; otherwise it contains a valid PO header. |
| 239 | |
| 240 | ```shell |
| 241 | # Get full context of local changes (HEAD vs working tree) |
| 242 | git-po-helper compare po/XX.po -o po/out.po |
| 243 | |
| 244 | # Get full context of changes in a specific commit (parent vs commit) |
| 245 | git-po-helper compare --commit <commit> po/XX.po -o po/out.po |
| 246 | |
| 247 | # Get full context of changes since a commit (commit vs working tree) |
| 248 | git-po-helper compare --since <commit> po/XX.po -o po/out.po |
| 249 | |
| 250 | # Get full context between two commits |
| 251 | git-po-helper compare -r <commit1>..<commit2> po/XX.po -o po/out.po |
| 252 | |
| 253 | # Get full context of two worktree files |
| 254 | git-po-helper compare po/old.po po/new.po -o po/out.po |
| 255 | |
| 256 | # Check msgid consistency (detect tampering); no output means target matches source |
| 257 | git-po-helper compare --msgid po/old.po po/new.po >po/out.po |
| 258 | ``` |
| 259 | |
| 260 | **Options summary** |
| 261 | |
| 262 | | Option | Meaning | |
| 263 | |---------------------|------------------------------------------------| |
| 264 | | (none) | Compare HEAD with working tree (local changes) | |
| 265 | | `--commit <commit>` | Compare parent of commit with the commit | |
| 266 | | `--since <commit>` | Compare commit with working tree | |
| 267 | | `-r x..y` | Compare revision x with revision y | |
| 268 | | `-r x..` | Compare revision x with working tree | |
| 269 | | `-r x` | Compare parent of x with x | |
| 270 | |
| 271 | |
| 272 | #### Concatenating multiple PO/JSON files |
| 273 | |
| 274 | `git-po-helper msg-cat` merges PO, POT, or gettext JSON inputs into one stream. |
| 275 | Duplicate `msgid` values keep the first occurrence in file order. Write with |
| 276 | `-o <file>` or stdout (`-o -` or omit); `--json` selects JSON output, else PO. |
| 277 | |
| 278 | ```shell |
| 279 | # Convert JSON to PO (e.g. after translation) |
| 280 | git-po-helper msg-cat --unset-fuzzy -o po/out.po po/in.json |
| 281 | |
| 282 | # Merge multiple PO files |
| 283 | git-po-helper msg-cat -o po/out.po po/in-1.po po/in-2.json |
| 284 | ``` |
| 285 | |
| 286 | |
| 287 | #### GETTEXT JSON format |
| 288 | |
| 289 | The **GETTEXT JSON** format is an internal format defined by `git-po-helper` |
| 290 | for convenient batch processing of translation and related tasks by AI models. |
| 291 | `git-po-helper msg-select`, `git-po-helper msg-cat`, and `git-po-helper compare` |
| 292 | read and write this format. |
| 293 | |
| 294 | **Top-level structure**: |
| 295 | |
| 296 | ```json |
| 297 | { |
| 298 | "header_comment": "string", |
| 299 | "header_meta": "string", |
| 300 | "entries": [ /* array of entry objects */ ] |
| 301 | } |
| 302 | ``` |
| 303 | |
| 304 | | Field | Description | |
| 305 | |------------------|--------------------------------------------------------------------------------| |
| 306 | | `header_comment` | Lines above the first `msgid ""` (comments, glossary), directly concatenated. | |
| 307 | | `header_meta` | Encoded `msgstr` of the header entry (Project-Id-Version, Plural-Forms, etc.). | |
| 308 | | `entries` | List of PO entries. Order matches source. | |
| 309 | |
| 310 | **Entry object** (each element of `entries`): |
| 311 | |
| 312 | | Field | Type | Description | |
| 313 | |-----------------|----------|--------------------------------------------------------------| |
| 314 | | `msgid` | string | Singular message ID. PO escapes encoded (e.g. `\n` → `\\n`). | |
| 315 | | `msgstr` | []string | Translation forms as a **JSON array only**. Details below. | |
| 316 | | `msgid_plural` | string | Plural form of msgid. Omit for non-plural. | |
| 317 | | `comments` | []string | Comment lines (`#`, `#.`, `#:`, `#,`, etc.). | |
| 318 | | `fuzzy` | bool | True if entry has fuzzy flag. | |
| 319 | | `obsolete` | bool | True for `#~` obsolete entries. Omit if false. | |
| 320 | |
| 321 | **`msgstr` array (required shape)**: |
| 322 | |
| 323 | - **Always** a JSON array of strings, never a single string. One element = singular |
| 324 | (PO `msgstr` / `msgstr[0]`); multiple elements = plural forms in order |
| 325 | (`msgstr[0]`, `msgstr[1]`, …). |
| 326 | - Omit the key or use an empty array when the entry is untranslated. |
| 327 | |
| 328 | **Example (single-line entry)**: |
| 329 | |
| 330 | ```json |
| 331 | { |
| 332 | "header_comment": "# Glossary:\\n# term1\\tTranslation 1\\n#\\n", |
| 333 | "header_meta": "Project-Id-Version: git\\nContent-Type: text/plain; charset=UTF-8\\n", |
| 334 | "entries": [ |
| 335 | { |
| 336 | "msgid": "Hello", |
| 337 | "msgstr": ["你好"], |
| 338 | "comments": ["#. Comment for translator\\n", "#: src/file.c:10\\n"], |
| 339 | "fuzzy": false |
| 340 | } |
| 341 | ] |
| 342 | } |
| 343 | ``` |
| 344 | |
| 345 | **Example (plural entry)**: |
| 346 | |
| 347 | ```json |
| 348 | { |
| 349 | "msgid": "One file", |
| 350 | "msgid_plural": "%d files", |
| 351 | "msgstr": ["一个文件", "%d 个文件"], |
| 352 | "comments": ["#, c-format\\n"] |
| 353 | } |
| 354 | ``` |
| 355 | |
| 356 | **Example (fuzzy entry before translation)**: |
| 357 | |
| 358 | ```json |
| 359 | { |
| 360 | "msgid": "Old message", |
| 361 | "msgstr": ["旧翻译。"], |
| 362 | "comments": ["#, fuzzy\\n"], |
| 363 | "fuzzy": true |
| 364 | } |
| 365 | ``` |
| 366 | |
| 367 | **Translation notes for GETTEXT JSON files**: |
| 368 | |
| 369 | - **Preserve structure**: Keep `header_comment`, `header_meta`, `msgid`, |
| 370 | `msgid_plural` unchanged. |
| 371 | - **Fuzzy entries**: Entries extracted from fuzzy PO entries have `"fuzzy": true`. |
| 372 | After translating, **remove the `fuzzy` field** or set it to `false` in the |
| 373 | output JSON. The merge step uses `--unset-fuzzy`, which can also remove the |
| 374 | `fuzzy` field. |
| 375 | - **Placeholders**: Preserve `%s`, `%d`, etc. exactly; use `%n$` when |
| 376 | reordering (see "Placeholder Reordering" above). |
| 377 | |
| 378 | |
| 379 | ### Quality checklist |
| 380 | |
| 381 | - **Accuracy**: Faithful to original meaning; no omissions or distortions. |
| 382 | - **Fuzzy entries**: Re-translate fully and clear the fuzzy flag (see |
| 383 | "Translating fuzzy entries" above). |
| 384 | - **Terminology**: Consistent with glossary (see "Glossary Section" above) or |
| 385 | domain standards. |
| 386 | - **Grammar and fluency**: Correct and natural in the target language. |
| 387 | - **Placeholders**: Preserve variables (`%s`, `{name}`, `$1`) exactly; use |
| 388 | positional parameters when reordering (see "Placeholder Reordering" above). |
| 389 | - **Special characters**: Preserve escape sequences (`\n`, `\"`, `\\`, `\t`), |
| 390 | placeholders exactly as in `msgid`. See "Preserving Special Characters" above. |
| 391 | - **Plurals and gender**: Correct forms and agreement. |
| 392 | - **Context fit**: Suitable for UI space, tone, and use (e.g. error vs. tooltip). |
| 393 | - **Cultural appropriateness**: No offensive or ambiguous content. |
| 394 | - **Consistency**: Match prior translations of the same source. |
| 395 | - **Technical integrity**: Do not translate code, paths, commands, brands, or |
| 396 | proper nouns. |
| 397 | - **Readability**: Clear, concise, and user-friendly. |
| 398 | |
| 399 | |
| 400 | ## Housekeeping tasks for localization workflows |
| 401 | |
| 402 | For common housekeeping tasks, follow the steps in the matching subsection |
| 403 | below. |
| 404 | |
| 405 | |
| 406 | ### Task 1: Generating or updating po/git.pot |
| 407 | |
| 408 | When asked to generate or update `po/git.pot` (or the like): |
| 409 | |
| 410 | 1. **Directly execute** the command `make po/git.pot` without checking |
| 411 | if the file exists beforehand. |
| 412 | |
| 413 | 2. **Do not verify** the generated file after execution. Simply run the |
| 414 | command and consider the task complete. |
| 415 | |
| 416 | |
| 417 | ### Task 2: Updating po/XX.po |
| 418 | |
| 419 | When asked to update `po/XX.po` (or the like): |
| 420 | |
| 421 | 1. **Directly execute** the command `make po-update PO_FILE=po/XX.po` |
| 422 | without reading or checking the file content beforehand. |
| 423 | |
| 424 | 2. **Do not verify, translate, or review** the updated file after execution. |
| 425 | Simply run the command and consider the task complete. |
| 426 | |
| 427 | |
| 428 | ### Task 3: Translating po/XX.po |
| 429 | |
| 430 | To translate `po/XX.po`, use the steps below. The script uses gettext or |
| 431 | `git-po-helper` depending on what is installed; JSON export (when available) |
| 432 | supports batch translation rather than per-entry work. |
| 433 | |
| 434 | **Workflow loop**: Steps 1→2→3→4→5→6→7 form a loop. After step 6 succeeds, |
| 435 | **always** go to step 7, which returns to step 1. The **only** exit to step 8 |
| 436 | is when step 2 finds `po/l10n-pending.po` empty. Do not skip step 7 or jump to |
| 437 | step 8 after step 6. |
| 438 | |
| 439 | 1. **Extract entries to translate**: **Directly execute** the script below—it is |
| 440 | authoritative; do not reimplement. It generates `po/l10n-pending.po` with |
| 441 | messages that need translation. |
| 442 | |
| 443 | ```shell |
| 444 | l10n_extract_pending () { |
| 445 | test $# -ge 1 || { echo "Usage: l10n_extract_pending <po-file>" >&2; return 1; } |
| 446 | PO_FILE="$1" |
| 447 | PENDING="po/l10n-pending.po" |
| 448 | PENDING_FUZZY="${PENDING}.fuzzy" |
| 449 | PENDING_REFER="${PENDING}.fuzzy.reference" |
| 450 | PENDING_UNTRANS="${PENDING}.untranslated" |
| 451 | rm -f "$PENDING" |
| 452 | |
| 453 | if command -v git-po-helper >/dev/null 2>&1 |
| 454 | then |
| 455 | git-po-helper msg-select --untranslated --fuzzy --no-obsolete -o "$PENDING" "$PO_FILE" |
| 456 | else |
| 457 | msgattrib --untranslated --no-obsolete "$PO_FILE" >"${PENDING_UNTRANS}" |
| 458 | msgattrib --only-fuzzy --no-obsolete --clear-fuzzy --empty "$PO_FILE" >"${PENDING_FUZZY}" |
| 459 | msgattrib --only-fuzzy --no-obsolete "$PO_FILE" >"${PENDING_REFER}" |
| 460 | msgcat --use-first "${PENDING_UNTRANS}" "${PENDING_FUZZY}" >"$PENDING" |
| 461 | rm -f "${PENDING_UNTRANS}" "${PENDING_FUZZY}" |
| 462 | fi |
| 463 | if test -s "$PENDING" |
| 464 | then |
| 465 | msgfmt --stat -o /dev/null "$PENDING" || true |
| 466 | echo "Pending file is not empty; there are still entries to translate." |
| 467 | else |
| 468 | echo "No entries need translation." |
| 469 | return 1 |
| 470 | fi |
| 471 | } |
| 472 | # Run the extraction. Example: l10n_extract_pending po/zh_CN.po |
| 473 | l10n_extract_pending po/XX.po |
| 474 | ``` |
| 475 | |
| 476 | 2. **Check generated file**: If `po/l10n-pending.po` is empty or does not exist, |
| 477 | translation is complete; go to step 8. Otherwise proceed to step 3. |
| 478 | |
| 479 | 3. **Prepare one batch for translation**: Batching keeps each run small so the |
| 480 | model can complete translation within limited context. **BEFORE translating**, |
| 481 | **directly execute** the script below—it is authoritative; do not reimplement. |
| 482 | Based on which file the script produces: if `po/l10n-todo.json` exists, go to |
| 483 | step 4a; if `po/l10n-todo.po` exists, go to step 4b. |
| 484 | |
| 485 | ```shell |
| 486 | l10n_one_batch () { |
| 487 | test $# -ge 1 || { echo "Usage: l10n_one_batch <po-file> [min_batch_size]" >&2; return 1; } |
| 488 | PO_FILE="$1" |
| 489 | min_batch_size=${2:-100} |
| 490 | PENDING="po/l10n-pending.po" |
| 491 | TODO_JSON="po/l10n-todo.json" |
| 492 | TODO_PO="po/l10n-todo.po" |
| 493 | DONE_JSON="po/l10n-done.json" |
| 494 | DONE_PO="po/l10n-done.po" |
| 495 | rm -f "$TODO_JSON" "$TODO_PO" "$DONE_JSON" "$DONE_PO" |
| 496 | |
| 497 | ENTRY_COUNT=$(grep -c '^msgid ' "$PENDING" 2>/dev/null || echo 0) |
| 498 | ENTRY_COUNT=$((ENTRY_COUNT > 0 ? ENTRY_COUNT - 1 : 0)) |
| 499 | |
| 500 | if test "$ENTRY_COUNT" -gt $min_batch_size |
| 501 | then |
| 502 | if test "$ENTRY_COUNT" -gt $((min_batch_size * 8)) |
| 503 | then |
| 504 | NUM=$((min_batch_size * 2)) |
| 505 | elif test "$ENTRY_COUNT" -gt $((min_batch_size * 4)) |
| 506 | then |
| 507 | NUM=$((min_batch_size + min_batch_size / 2)) |
| 508 | else |
| 509 | NUM=$min_batch_size |
| 510 | fi |
| 511 | BATCHING=1 |
| 512 | else |
| 513 | NUM=$ENTRY_COUNT |
| 514 | BATCHING= |
| 515 | fi |
| 516 | |
| 517 | if command -v git-po-helper >/dev/null 2>&1 |
| 518 | then |
| 519 | if test -n "$BATCHING" |
| 520 | then |
| 521 | git-po-helper msg-select --json --head "$NUM" -o "$TODO_JSON" "$PENDING" |
| 522 | echo "Processing batch of $NUM entries (out of $ENTRY_COUNT remaining)" |
| 523 | else |
| 524 | git-po-helper msg-select --json -o "$TODO_JSON" "$PENDING" |
| 525 | echo "Processing all $ENTRY_COUNT entries at once" |
| 526 | fi |
| 527 | else |
| 528 | if test -n "$BATCHING" |
| 529 | then |
| 530 | awk -v num="$NUM" '/^msgid / && count++ > num {exit} 1' "$PENDING" | |
| 531 | tac | awk '/^$/ {found=1} found' | tac >"$TODO_PO" |
| 532 | echo "Processing batch of $NUM entries (out of $ENTRY_COUNT remaining)" |
| 533 | else |
| 534 | cp "$PENDING" "$TODO_PO" |
| 535 | echo "Processing all $ENTRY_COUNT entries at once" |
| 536 | fi |
| 537 | fi |
| 538 | } |
| 539 | # Prepare one batch; shrink 2nd arg when batches exceed agent capacity. |
| 540 | l10n_one_batch po/XX.po 100 |
| 541 | ``` |
| 542 | |
| 543 | 4a. **Translate JSON batch** (`po/l10n-todo.json` → `po/l10n-done.json`): |
| 544 | |
| 545 | - **Task**: Translate `po/l10n-todo.json` (input, GETTEXT JSON) into |
| 546 | `po/l10n-done.json` (output, GETTEXT JSON). See the "GETTEXT JSON format" |
| 547 | section above for format details and translation rules. |
| 548 | - **Reference glossary**: Read the glossary from the batch file's |
| 549 | `header_comment` (see "Glossary Section" above) and use it for |
| 550 | consistent terminology. |
| 551 | - **When translating**: Follow the "Quality checklist" above for correctness |
| 552 | and quality. Handle escape sequences (`\n`, `\"`, `\\`, `\t`), placeholders, |
| 553 | and quotes correctly as in `msgid`. For JSON, correctly escape and unescape |
| 554 | these sequences when reading and writing. Modify `msgstr` and `msgstr[n]` |
| 555 | (for plural entries); clear the fuzzy flag (omit or set `fuzzy` to `false`). |
| 556 | Do **not** modify `msgid` or `msgid_plural`. |
| 557 | |
| 558 | 4b. **Translate PO batch** (`po/l10n-todo.po` → `po/l10n-done.po`): |
| 559 | |
| 560 | - **Task**: Translate `po/l10n-todo.po` (input, GETTEXT PO) into |
| 561 | `po/l10n-done.po` (output, GETTEXT PO). |
| 562 | - **Reference glossary**: Read the glossary from the pending file header |
| 563 | (see "Glossary Section" above) and use it for consistent terminology. |
| 564 | - **When translating**: Follow the "Quality checklist" above for correctness |
| 565 | and quality. Preserve escape sequences (`\n`, `\"`, `\\`, `\t`), placeholders, |
| 566 | and quotes as in `msgid`. Modify `msgstr` and `msgstr[n]` (for plural |
| 567 | entries); remove the `#, fuzzy` tag from comments when done. Do **not** |
| 568 | modify `msgid` or `msgid_plural`. |
| 569 | |
| 570 | 5. **Validate `po/l10n-done.po`**: |
| 571 | |
| 572 | Run the validation script below. If it fails, fix per the errors and notes, |
| 573 | re-run until it succeeds. |
| 574 | |
| 575 | ```shell |
| 576 | l10n_validate_done () { |
| 577 | DONE_PO="po/l10n-done.po" |
| 578 | DONE_JSON="po/l10n-done.json" |
| 579 | PENDING="po/l10n-pending.po" |
| 580 | |
| 581 | if test -f "$DONE_JSON" && { ! test -f "$DONE_PO" || test "$DONE_JSON" -nt "$DONE_PO"; } |
| 582 | then |
| 583 | git-po-helper msg-cat --unset-fuzzy -o "$DONE_PO" "$DONE_JSON" || { |
| 584 | echo "ERROR [JSON to PO conversion]: Fix $DONE_JSON and re-run." >&2 |
| 585 | return 1 |
| 586 | } |
| 587 | fi |
| 588 | |
| 589 | # Check 1: msgid should not be modified |
| 590 | MSGID_OUT=$(git-po-helper compare -q --msgid --assert-no-changes \ |
| 591 | "$PENDING" "$DONE_PO" 2>&1) |
| 592 | MSGID_RC=$? |
| 593 | if test $MSGID_RC -ne 0 || test -n "$MSGID_OUT" |
| 594 | then |
| 595 | echo "ERROR [msgid modified]: The following entries appeared after" >&2 |
| 596 | echo "translation because msgid was altered. Fix in $DONE_PO." >&2 |
| 597 | echo "$MSGID_OUT" >&2 |
| 598 | return 1 |
| 599 | fi |
| 600 | |
| 601 | # Check 2: PO format (see "Validating PO File Format" for error handling) |
| 602 | MSGFMT_OUT=$(msgfmt --check -o /dev/null "$DONE_PO" 2>&1) |
| 603 | MSGFMT_RC=$? |
| 604 | if test $MSGFMT_RC -ne 0 |
| 605 | then |
| 606 | echo "ERROR [PO format]: Fix errors in $DONE_PO." >&2 |
| 607 | echo "$MSGFMT_OUT" >&2 |
| 608 | return 1 |
| 609 | fi |
| 610 | |
| 611 | echo "Validation passed." |
| 612 | } |
| 613 | l10n_validate_done |
| 614 | ``` |
| 615 | |
| 616 | If the script fails, fix **directly in `po/l10n-done.po`**. Re-run |
| 617 | `l10n_validate_done` until it succeeds. Editing `po/l10n-done.json` is not |
| 618 | recommended because it adds an extra JSON-to-PO conversion step. Use the |
| 619 | error message to decide: |
| 620 | |
| 621 | - **`[msgid modified]`**: The listed entries have altered `msgid`; restore |
| 622 | them to match `po/l10n-pending.po`. |
| 623 | - **`[PO format]`**: `msgfmt` reports line numbers; fix the errors in place. |
| 624 | See "Validating PO File Format" for common issues. |
| 625 | |
| 626 | |
| 627 | 6. **Merge translation results into `po/XX.po`**: Run the script below. If it |
| 628 | fails, fix the file the error names: **`[JSON to PO conversion]`** → |
| 629 | `po/l10n-done.json`; **`[msgcat merge]`** → `po/l10n-done.po`. Re-run until |
| 630 | it succeeds. |
| 631 | |
| 632 | ```shell |
| 633 | l10n_merge_batch () { |
| 634 | test $# -ge 1 || { echo "Usage: l10n_merge_batch <po-file>" >&2; return 1; } |
| 635 | PO_FILE="$1" |
| 636 | DONE_PO="po/l10n-done.po" |
| 637 | DONE_JSON="po/l10n-done.json" |
| 638 | MERGED="po/l10n-done.merged" |
| 639 | PENDING="po/l10n-pending.po" |
| 640 | PENDING_REFER="${PENDING}.fuzzy.reference" |
| 641 | TODO_JSON="po/l10n-todo.json" |
| 642 | TODO_PO="po/l10n-todo.po" |
| 643 | if test -f "$DONE_JSON" && { ! test -f "$DONE_PO" || test "$DONE_JSON" -nt "$DONE_PO"; } |
| 644 | then |
| 645 | git-po-helper msg-cat --unset-fuzzy -o "$DONE_PO" "$DONE_JSON" || { |
| 646 | echo "ERROR [JSON to PO conversion]: Fix $DONE_JSON and re-run." >&2 |
| 647 | return 1 |
| 648 | } |
| 649 | fi |
| 650 | msgcat --use-first "$DONE_PO" "$PO_FILE" >"$MERGED" || { |
| 651 | echo "ERROR [msgcat merge]: Fix errors in $DONE_PO and re-run." >&2 |
| 652 | return 1 |
| 653 | } |
| 654 | mv "$MERGED" "$PO_FILE" |
| 655 | rm -f "$TODO_JSON" "$TODO_PO" "$DONE_JSON" "$DONE_PO" "$PENDING_REFER" |
| 656 | } |
| 657 | # Run the merge. Example: l10n_merge_batch po/zh_CN.po |
| 658 | l10n_merge_batch po/XX.po |
| 659 | ``` |
| 660 | |
| 661 | 7. **Loop**: **MUST** return to step 1 (Extract entries) and repeat the cycle. |
| 662 | Do **not** skip this step or go to step 8. Step 8 (below) runs **only** |
| 663 | when step 2 finds no more entries and redirects there. |
| 664 | |
| 665 | 8. **Only after loop exits**: Run the command below to validate the PO file and |
| 666 | display the report. The process ends here. |
| 667 | |
| 668 | ```shell |
| 669 | msgfmt --check --stat -o /dev/null po/XX.po |
| 670 | ``` |
| 671 | |
| 672 | |
| 673 | ### Task 4: Review translation quality |
| 674 | |
| 675 | Review may target the full `po/XX.po`, a specific commit, or changes since a |
| 676 | commit. When asked to review, follow the steps below. |
| 677 | |
| 678 | **Workflow**: Follow steps in order. Do **NOT** use `git show`, `git diff`, |
| 679 | `git format-patch`, or similar to get changes—they break PO context; use **only** |
| 680 | `git-po-helper compare` for extraction. Without `git-po-helper`, refuse the task. |
| 681 | Steps 3→4→5→6→7 loop: after step 6, **always** go to step 7 (back to step 3). |
| 682 | The **only** ways to step 8 are when step 4 finds `po/review-todo.json` missing |
| 683 | or empty (no batch left to review), or when step 1 finds `po/review-result.json` |
| 684 | already present. |
| 685 | |
| 686 | 1. **Check for existing review (resume support)**: Evaluate the following in order: |
| 687 | |
| 688 | - If `po/review-input.po` does **not** exist, proceed to step 2 (Extract |
| 689 | entries) for a fresh start. |
| 690 | - Else If `po/review-result.json` exists, go to step 8 (only after loop exits). |
| 691 | - Else If `po/review-done.json` exists, go to step 6 (Rename result). |
| 692 | - Else if `po/review-todo.json` exists, go to step 5 (Review the current |
| 693 | batch). |
| 694 | - Else go to step 3 (Prepare one batch). |
| 695 | |
| 696 | 2. **Extract entries**: Run `git-po-helper compare` with the desired range and |
| 697 | redirect the output to `po/review-input.po`. See "Comparing PO files for |
| 698 | translation and review" under git-po-helper for options. |
| 699 | |
| 700 | 3. **Prepare one batch**: Batching keeps each run small so the model can |
| 701 | complete review within limited context. **Directly execute** the script |
| 702 | below—it is authoritative; do not reimplement. |
| 703 | |
| 704 | ```shell |
| 705 | review_one_batch () { |
| 706 | min_batch_size=${1:-100} |
| 707 | INPUT_PO="po/review-input.po" |
| 708 | PENDING="po/review-pending.po" |
| 709 | TODO="po/review-todo.json" |
| 710 | DONE="po/review-done.json" |
| 711 | BATCH_FILE="po/review-batch.txt" |
| 712 | |
| 713 | if test ! -f "$INPUT_PO" |
| 714 | then |
| 715 | rm -f "$TODO" |
| 716 | echo >&2 "cannot find $INPUT_PO, nothing for review" |
| 717 | return 1 |
| 718 | fi |
| 719 | if test ! -f "$PENDING" || test "$INPUT_PO" -nt "$PENDING" |
| 720 | then |
| 721 | rm -f "$BATCH_FILE" "$TODO" "$DONE" |
| 722 | rm -f po/review-result*.json |
| 723 | cp "$INPUT_PO" "$PENDING" |
| 724 | fi |
| 725 | |
| 726 | ENTRY_COUNT=$(grep -c '^msgid ' "$PENDING" 2>/dev/null || echo 0) |
| 727 | ENTRY_COUNT=$((ENTRY_COUNT > 0 ? ENTRY_COUNT - 1 : 0)) |
| 728 | if test "$ENTRY_COUNT" -eq 0 |
| 729 | then |
| 730 | rm -f "$TODO" |
| 731 | echo >&2 "No entries left for review" |
| 732 | return 1 |
| 733 | fi |
| 734 | |
| 735 | if test "$ENTRY_COUNT" -gt $min_batch_size |
| 736 | then |
| 737 | if test "$ENTRY_COUNT" -gt $((min_batch_size * 8)) |
| 738 | then |
| 739 | NUM=$((min_batch_size * 2)) |
| 740 | elif test "$ENTRY_COUNT" -gt $((min_batch_size * 4)) |
| 741 | then |
| 742 | NUM=$((min_batch_size + min_batch_size / 2)) |
| 743 | else |
| 744 | NUM=$min_batch_size |
| 745 | fi |
| 746 | else |
| 747 | NUM=$ENTRY_COUNT |
| 748 | fi |
| 749 | |
| 750 | BATCH=$(cat "$BATCH_FILE" 2>/dev/null || echo 0) |
| 751 | BATCH=$((BATCH + 1)) |
| 752 | echo "$BATCH" >"$BATCH_FILE" |
| 753 | |
| 754 | git-po-helper msg-select --json --head "$NUM" -o "$TODO" "$PENDING" |
| 755 | git-po-helper msg-select --since "$((NUM + 1))" -o "${PENDING}.tmp" "$PENDING" |
| 756 | mv "${PENDING}.tmp" "$PENDING" |
| 757 | echo "Processing batch $BATCH ($NUM entries out of $ENTRY_COUNT)" |
| 758 | } |
| 759 | # The parameter controls batch size; reduce if the batch file is too large. |
| 760 | review_one_batch 100 |
| 761 | ``` |
| 762 | |
| 763 | 4. **Check todo file**: If `po/review-todo.json` does not exist or is empty, |
| 764 | review is complete; go to step 8 (only after loop exits). Otherwise proceed to |
| 765 | step 5. |
| 766 | |
| 767 | 5. **Review the current batch**: Review translations in `po/review-todo.json` |
| 768 | and write findings to `po/review-done.json` as follows: |
| 769 | - Use "Background knowledge for localization workflows" for PO/JSON structure, |
| 770 | placeholders, and terminology. |
| 771 | - If `header_comment` includes a glossary, follow it for consistency. |
| 772 | - Do **not** review the header (`header_comment`, `header_meta`). |
| 773 | - For every other entry, check the entry's `msgstr` **array** (translation |
| 774 | forms) against `msgid` / `msgid_plural` using the "Quality checklist" above. |
| 775 | - Write JSON per "Review result JSON format" below; use `{"issues": []}` when |
| 776 | there are no issues. **Always** write `po/review-done.json`—it marks the |
| 777 | batch complete. |
| 778 | |
| 779 | 6. **Rename result**: Rename `po/review-done.json` to `po/review-result-<N>.json`, |
| 780 | where N is the value in `po/review-batch.txt` (the batch just completed). |
| 781 | Run the script below: |
| 782 | |
| 783 | ```shell |
| 784 | review_rename_result () { |
| 785 | TODO="po/review-todo.json" |
| 786 | DONE="po/review-done.json" |
| 787 | BATCH_FILE="po/review-batch.txt" |
| 788 | if test -f "$DONE" |
| 789 | then |
| 790 | N=$(cat "$BATCH_FILE" 2>/dev/null) || { echo "ERROR: $BATCH_FILE not found." >&2; return 1; } |
| 791 | mv "$DONE" "po/review-result-$N.json" |
| 792 | echo "Renamed to po/review-result-$N.json" |
| 793 | fi |
| 794 | rm -f "$TODO" |
| 795 | } |
| 796 | review_rename_result |
| 797 | ``` |
| 798 | |
| 799 | 7. **Loop**: **MUST** return to step 3 (Prepare one batch) and repeat the cycle. |
| 800 | Do **not** skip this step or go to step 8. Step 8 is reached **only** when |
| 801 | step 4 finds `po/review-todo.json` missing or empty. |
| 802 | |
| 803 | 8. **Only after loop exits**: **Directly execute** the command below. It merges |
| 804 | results, applies suggestions, and displays the report. The process ends here. |
| 805 | |
| 806 | ```shell |
| 807 | git-po-helper agent-run review --report po |
| 808 | ``` |
| 809 | |
| 810 | **Do not** run cleanup or delete intermediate files. Keep them for inspection |
| 811 | or resumption. |
| 812 | |
| 813 | **Review result JSON format**: |
| 814 | |
| 815 | The **Review result JSON** format defines the structure for translation |
| 816 | review reports. For each entry with translation issues, create an issue |
| 817 | object as follows: |
| 818 | |
| 819 | - Copy the original entry's `msgid`, optional `msgid_plural`, and optional |
| 820 | `msgstr` array (original translation forms) into the issue object. Use the |
| 821 | same shape as GETTEXT JSON: `msgstr` is **always a JSON array** when present |
| 822 | (one element singular, multiple for plural). |
| 823 | - Write a summary of all issues found for this entry in `description`. |
| 824 | - Set `score` according to the severity of issues found for this entry, |
| 825 | from 0 to 3 (0 = critical; 1 = major; 2 = minor; 3 = perfect, no issues). |
| 826 | **Lower score means more severe issues.** |
| 827 | - Place the suggested translation in **`suggest_msgstr`** as a **JSON array**: |
| 828 | one string for singular, multiple strings for plural forms in order. This is |
| 829 | required for `git-po-helper` to apply suggestions. |
| 830 | - Include only entries with issues (score less than 3). When no issues are |
| 831 | found in the batch, write `{"issues": []}`. |
| 832 | |
| 833 | Example review result (with issues): |
| 834 | |
| 835 | ```json |
| 836 | { |
| 837 | "issues": [ |
| 838 | { |
| 839 | "msgid": "commit", |
| 840 | "msgstr": ["委托"], |
| 841 | "score": 0, |
| 842 | "description": "Terminology error: 'commit' should be translated as '提交'", |
| 843 | "suggest_msgstr": ["提交"] |
| 844 | }, |
| 845 | { |
| 846 | "msgid": "repository", |
| 847 | "msgid_plural": "repositories", |
| 848 | "msgstr": ["版本库", "版本库"], |
| 849 | "score": 2, |
| 850 | "description": "Consistency issue: suggest using '仓库' consistently", |
| 851 | "suggest_msgstr": ["仓库", "仓库"] |
| 852 | } |
| 853 | ] |
| 854 | } |
| 855 | ``` |
| 856 | |
| 857 | Field descriptions for each issue object (element of the `issues` array): |
| 858 | |
| 859 | - `msgid` (and optional `msgid_plural` for plural entries): Original source text. |
| 860 | - `msgstr` (optional): JSON array of original translation forms (same meaning as |
| 861 | in GETTEXT JSON entries). |
| 862 | - `suggest_msgstr`: JSON array of suggested translation forms; **must be an |
| 863 | array** (e.g. `["提交"]` for singular). Plural entries use multiple elements |
| 864 | in order. |
| 865 | - `score`: 0–3 (0 = critical; 1 = major; 2 = minor; 3 = perfect, no issues). |
| 866 | - `description`: Brief summary of the issue. |
| 867 | |
| 868 | |
| 869 | ## Human translators remain in control |
| 870 | |
| 871 | Git translation is human-driven; language team leaders and contributors are |
| 872 | responsible for maintaining translation quality and consistency. |
| 873 | |
| 874 | AI-generated output should always be treated as drafts that must be reviewed |
| 875 | and approved by someone who understands both the technical context and the |
| 876 | target language. The best results come from combining AI efficiency with human |
| 877 | judgment, cultural insight, and community engagement. |