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