Rendered Raw
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.