Skip to content

Commit 1b4d6bf

Browse files
josixclaude
andcommitted
Add translation-memory lookup to Claude skills via agentic-retrieval
Add .scripts/translation_memory.sh and .scripts/translation_memory/main.py, a stdlib-only helper that indexes this repo's .po files with agentic-retrieval (lexical retriever, cjk tokenizer) and returns similar already-translated entries as msgid/msgstr pairs, filtering out fuzzy and untranslated ones. The engine is located via AGENTIC_RETRIEVAL_ENGINE or a `retrieval` command on PATH; the helper exits 3 with a hint otherwise. translate-po, check-terminology and validate-translation gain a lookup step that treats these hits as reference only (glossary and README rules still win) and skips silently when the helper is unavailable. README documents setup and usage; CLAUDE.md gains a pointer and a corrected line range. The index directory .agentic-retrieval/ is gitignored. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent ae5803e commit 1b4d6bf

8 files changed

Lines changed: 306 additions & 4 deletions

File tree

‎.claude/skills/check-terminology/SKILL.md‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Check terminology against project glossary and zh_CN/zh_TW mappings.
1313
1. **Primary:** `glossary.po` - Official Python glossary translations
1414
2. **Secondary:** `focused_terminology_dictionary.csv` - 118 key terms
1515
3. **Tertiary:** `.scripts/google_translate/utils.py` - zh_CN to zh_TW mappings
16+
4. **Usage evidence (not authority):** `.scripts/translation_memory.sh query "<term>"` - how existing translated .po entries render the term (skip if the command fails)
1617

1718
## Check Process
1819

@@ -21,12 +22,14 @@ Check terminology against project glossary and zh_CN/zh_TW mappings.
2122
2. Check if it's in `references/keep-in-english.md`
2223
3. If found, use the standard translation
2324
4. If not found, check `glossary.po` in project root
25+
5. Run `.scripts/translation_memory.sh query "<term>" --top-k 10` to see existing renderings with `path:line`; if the glossary is silent, report the dominant rendering as a suggestion
2426

2527
### For a PO File
2628
1. Extract all translated terms from msgstr
2729
2. Cross-reference against glossary
2830
3. Identify forbidden zh_CN terms
29-
4. Report inconsistencies
31+
4. For each term that looks inconsistent, run `.scripts/translation_memory.sh query "<English term>" --top-k 10` and compare renderings across files
32+
5. Report inconsistencies, citing `path:line` from the lookup
3033

3134
## Error Categories
3235

@@ -41,6 +44,10 @@ Example violations:
4144
### Category 2: Inconsistent Terms (Should Review)
4245
Same English term translated differently across files.
4346

47+
Gather evidence with the translation memory lookup and cite each rendering with
48+
`path:line`. The glossary decides; majority usage only informs a suggestion
49+
when the glossary is silent.
50+
4451
### Category 3: Missing from Glossary (Informational)
4552
New terms not yet in the glossary.
4653

@@ -71,6 +78,7 @@ Decision Log:
7178

7279
1. Check `../translate-po/references/terminology.md` first (authoritative, 186 terms)
7380
2. If term should stay in English, see `references/keep-in-english.md`
81+
3. To see how a term is actually rendered across files: `.scripts/translation_memory.sh query "<term>"`
7482

7583
## References
7684

‎.claude/skills/translate-po/SKILL.md‎

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,27 @@ Translate English PO msgid entries to Traditional Chinese msgstr following all p
1515
3. Look up terminology in `references/terminology.md`
1616
4. Check `references/forbidden-terms.md` for zh_CN terms to avoid
1717

18+
## Translation Memory Lookup
19+
20+
Before translating an entry that contains prose, look up how similar text was
21+
already translated in this repo:
22+
23+
```bash
24+
.scripts/translation_memory.sh query "<msgid text or its key English terms>"
25+
```
26+
27+
- Prints up to 5 translated, non-fuzzy entries as `path:line` followed by
28+
`msgid:` / `msgstr:` lines (use `--top-k N` for more).
29+
- To see how one term is rendered elsewhere, query the term alone
30+
(e.g. `"context manager"`) or a candidate Chinese rendering of 2+ characters
31+
(e.g. `"情境管理器"`).
32+
- Treat hits as reference for phrasing and term choice, not as authority:
33+
`references/terminology.md`, `glossary.po`, and the rules below win when they
34+
disagree, and existing translations may contain mistakes.
35+
- Skip the lookup for code-only entries and very short entries.
36+
- If the command fails or prints nothing (for example, the engine is not
37+
installed), continue without it and do not report the failure.
38+
1839
## Translation Process
1940

2041
### Step 1: Content Analysis
@@ -50,8 +71,10 @@ See `references/rst-cheatsheet.md` for complete rules.
5071

5172
1. Check term in `references/terminology.md`
5273
2. If not found, check `glossary.po` in project root
53-
3. If uncertain, add `#, fuzzy` flag
54-
4. Never use zh_CN terms from `references/forbidden-terms.md`
74+
3. If still not found, run `.scripts/translation_memory.sh query "<term>"` and
75+
follow the dominant existing rendering
76+
4. If uncertain, add `#, fuzzy` flag
77+
5. Never use zh_CN terms from `references/forbidden-terms.md`
5578

5679
## Output Format
5780

@@ -71,6 +94,7 @@ Before outputting, verify:
7194
- [ ] reST syntax preserved
7295
- [ ] Terminology matches glossary
7396
- [ ] No forbidden zh_CN terms
97+
- [ ] Consistent with translation memory hits, unless they conflict with the glossary
7498

7599
## Examples
76100

@@ -130,3 +154,4 @@ msgstr "可能需要審核的翻譯"
130154
- `references/terminology.md` - Key term translations (186 terms)
131155
- `references/forbidden-terms.md` - zh_CN to zh_TW mappings
132156
- `references/rst-cheatsheet.md` - reST syntax rules
157+
- `.scripts/translation_memory.sh` - Translation memory lookup over this repo's .po files

‎.claude/skills/validate-translation/SKILL.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Validate translated PO entries against project rules and report issues.
1414
- Compare terms against `glossary.po` and terminology dictionary
1515
- Flag zh_CN terms that should be zh_TW (see `references/validation-rules.md`)
1616
- Check for inconsistent translations of the same term
17+
- Run `.scripts/translation_memory.sh query "<English term>" --top-k 10` to
18+
compare with how other files render it. Report a deviation from the
19+
dominant rendering as WARNING only when `glossary.po` and the terminology
20+
dictionary are silent. Skip silently if the command fails.
1721

1822
### 2. Punctuation Rules
1923
- Chinese text must use full-width: `「」()、,。:;!?`
@@ -56,3 +60,4 @@ After review, run: `make lint`, `make wrap`, `make build <file>.po`
5660
## References
5761

5862
- `references/validation-rules.md` - Complete validation checklist
63+
- `.scripts/translation_memory.sh` - Translation memory lookup (existing renderings with `path:line`)

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ venv/
1010
ENV/
1111
env.bak/
1212
venv.bak/
13+
14+
# agentic-retrieval translation memory index
15+
.agentic-retrieval/

‎.scripts/translation_memory.sh‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/bin/sh
2+
# Translation memory lookup over this repo's .po files (see translation_memory/main.py).
3+
exec python3 "$(dirname "$0")/translation_memory/main.py" "$@"
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""Translation memory lookup over this repo's .po files via agentic-retrieval.
2+
3+
Usage (run from anywhere; the repo root is always the index root)::
4+
5+
.scripts/translation_memory.sh index [--force]
6+
.scripts/translation_memory.sh query "<text>" [--top-k N] [--fetch N]
7+
[--include-fuzzy] [--include-untranslated] [--json] [--max-chars N]
8+
9+
Engine resolution:
10+
11+
1. ``AGENTIC_RETRIEVAL_ENGINE`` = path to an agentic-retrieval ``engine``
12+
directory (needs ``uv``); run with ``uv run --frozen``.
13+
2. Otherwise a ``retrieval`` executable on ``PATH``.
14+
3. Otherwise exit with status 3.
15+
16+
The index lives in ``.agentic-retrieval/`` (gitignored). ``query`` always runs
17+
``index`` first so the cjk tokenizer is guaranteed; a fresh index is a cheap
18+
no-op. Exit codes: 0 ok, 2 usage error, 3 engine unavailable or failed.
19+
Supports macOS and Linux only.
20+
"""
21+
22+
import argparse
23+
import json
24+
import os
25+
import shutil
26+
import subprocess
27+
import sys
28+
import time
29+
from pathlib import Path
30+
from typing import Any, Dict, List
31+
32+
REPO_ROOT = Path(__file__).resolve().parents[2]
33+
ENV_VAR = "AGENTIC_RETRIEVAL_ENGINE"
34+
EXIT_UNAVAILABLE = 3
35+
36+
37+
def fail(message: str) -> None:
38+
print(f"translation-memory: {message}", file=sys.stderr)
39+
sys.exit(EXIT_UNAVAILABLE)
40+
41+
42+
def resolve_engine_cmd() -> List[str]:
43+
engine = os.environ.get(ENV_VAR)
44+
if engine:
45+
if not (Path(engine) / "pyproject.toml").is_file():
46+
fail(
47+
f"{ENV_VAR}={engine} is not an agentic-retrieval engine "
48+
"directory (no pyproject.toml)"
49+
)
50+
if not shutil.which("uv"):
51+
fail(
52+
"'uv' not found on PATH (needed to run the engine from "
53+
f"{ENV_VAR})"
54+
)
55+
return ["uv", "run", "--frozen", "--quiet", "--directory", engine,
56+
"retrieval"]
57+
found = shutil.which("retrieval")
58+
if found:
59+
return [found]
60+
fail(
61+
"agentic-retrieval engine not found. Either\n"
62+
f" export {ENV_VAR}=/path/to/agentic-retrieval/engine\n"
63+
"or install it so that 'retrieval' is on PATH."
64+
)
65+
return [] # unreachable
66+
67+
68+
def run_engine(engine_cmd: List[str], args: List[str]) -> str:
69+
try:
70+
proc = subprocess.run(
71+
engine_cmd + args, capture_output=True, encoding="utf-8"
72+
)
73+
except OSError as exc:
74+
fail(f"cannot run engine: {exc}")
75+
if proc.returncode != 0:
76+
sys.stderr.write(proc.stdout or "")
77+
sys.stderr.write(proc.stderr or "")
78+
fail(f"engine exited with status {proc.returncode}")
79+
return proc.stdout
80+
81+
82+
def run_index(engine_cmd: List[str], force: bool) -> None:
83+
args = ["index", "--root", str(REPO_ROOT), "--retriever", "lexical",
84+
"--tokenizer", "cjk"]
85+
if force:
86+
args.append("--force")
87+
out = run_engine(engine_cmd, args)
88+
if out.strip():
89+
sys.stderr.write(out)
90+
91+
92+
def cmd_index(args: argparse.Namespace) -> int:
93+
start = time.monotonic()
94+
run_index(resolve_engine_cmd(), args.force)
95+
print(
96+
f"translation-memory: index ready in {time.monotonic() - start:.1f}s",
97+
file=sys.stderr,
98+
)
99+
return 0
100+
101+
102+
def keep_hit(meta: Dict[str, Any], args: argparse.Namespace) -> bool:
103+
if meta.get("kind") != "po_entry":
104+
return False
105+
if not args.include_untranslated and not meta.get("translated"):
106+
return False
107+
if not args.include_fuzzy and meta.get("fuzzy"):
108+
return False
109+
return True
110+
111+
112+
def collect_entries(
113+
hits: List[Dict[str, Any]], args: argparse.Namespace
114+
) -> List[Dict[str, Any]]:
115+
entries: List[Dict[str, Any]] = []
116+
seen = set()
117+
for hit in hits:
118+
meta = hit.get("metadata") or {}
119+
if not keep_hit(meta, args):
120+
continue
121+
key = (meta.get("msgid"), meta.get("msgstr"))
122+
if key in seen:
123+
continue
124+
seen.add(key)
125+
path = hit.get("path", "")
126+
if os.path.isabs(path):
127+
path = os.path.relpath(path, REPO_ROOT)
128+
entry = {
129+
"path": path,
130+
"line": meta.get("line"),
131+
"msgid": meta.get("msgid", ""),
132+
"msgstr": meta.get("msgstr", ""),
133+
"fuzzy": bool(meta.get("fuzzy")),
134+
"translated": bool(meta.get("translated")),
135+
"references": meta.get("references") or [],
136+
}
137+
if meta.get("msgctxt"):
138+
entry["msgctxt"] = meta["msgctxt"]
139+
entries.append(entry)
140+
if len(entries) >= args.top_k:
141+
break
142+
return entries
143+
144+
145+
def compact(text: str, max_chars: int) -> str:
146+
text = " ".join(str(text).split())
147+
if max_chars and len(text) > max_chars:
148+
return text[:max_chars].rstrip() + "..."
149+
return text
150+
151+
152+
def format_entry(entry: Dict[str, Any], max_chars: int) -> str:
153+
header = f"{entry['path']}:{entry['line']}"
154+
if entry["fuzzy"]:
155+
header += " [fuzzy]"
156+
if not entry["translated"]:
157+
header += " [untranslated]"
158+
lines = [header]
159+
if "msgctxt" in entry:
160+
lines.append(f" msgctxt: {compact(entry['msgctxt'], max_chars)}")
161+
lines.append(f" msgid: {compact(entry['msgid'], max_chars)}")
162+
lines.append(f" msgstr: {compact(entry['msgstr'], max_chars)}")
163+
return "\n".join(lines)
164+
165+
166+
def cmd_query(args: argparse.Namespace) -> int:
167+
engine_cmd = resolve_engine_cmd()
168+
run_index(engine_cmd, False)
169+
fetch = args.fetch or max(3 * args.top_k, 20)
170+
out = run_engine(
171+
engine_cmd,
172+
["query", args.text, "--root", str(REPO_ROOT), "--retriever",
173+
"lexical", "--top-k", str(fetch), "--json", "--stale-ok"],
174+
)
175+
try:
176+
hits = json.loads(out).get("results", [])
177+
except ValueError as exc:
178+
fail(f"engine returned invalid JSON: {exc}")
179+
entries = collect_entries(hits, args)
180+
if args.json:
181+
print(json.dumps(entries, ensure_ascii=False, indent=2))
182+
elif entries:
183+
print("\n\n".join(format_entry(e, args.max_chars) for e in entries))
184+
if not entries:
185+
print("translation-memory: no matching translated entries",
186+
file=sys.stderr)
187+
return 0
188+
189+
190+
def build_parser() -> argparse.ArgumentParser:
191+
parser = argparse.ArgumentParser(
192+
description="Translation memory over this repo's .po files."
193+
)
194+
sub = parser.add_subparsers(dest="command", required=True)
195+
p_index = sub.add_parser("index", help="build or refresh the index")
196+
p_index.add_argument("--force", action="store_true")
197+
p_index.set_defaults(func=cmd_index)
198+
p_query = sub.add_parser("query", help="find similar translated entries")
199+
p_query.add_argument("text")
200+
p_query.add_argument("--top-k", type=int, default=5)
201+
p_query.add_argument("--fetch", type=int, default=0,
202+
help="hits to fetch before filtering "
203+
"(default max(3*top-k, 20))")
204+
p_query.add_argument("--include-fuzzy", action="store_true")
205+
p_query.add_argument("--include-untranslated", action="store_true")
206+
p_query.add_argument("--json", action="store_true")
207+
p_query.add_argument("--max-chars", type=int, default=300,
208+
help="truncate each field (0 = no truncation)")
209+
p_query.set_defaults(func=cmd_query)
210+
return parser
211+
212+
213+
def main() -> int:
214+
args = build_parser().parse_args()
215+
return args.func(args)
216+
217+
218+
if __name__ == "__main__":
219+
sys.exit(main())

‎CLAUDE.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ make progress # Check translation progress
3939

4040
- **Terminology:** `focused_terminology_dictionary.csv`, `glossary.po`
4141
- **zh_CN corrections:** `.scripts/google_translate/utils.py`
42-
- **Full guidelines:** `README.rst` (lines 240-377)
42+
- **Full guidelines:** `README.rst` (lines 240-368)
4343
- **Skills:** `.claude/skills/` for detailed translation workflows
44+
- **Translation memory:** `.scripts/translation_memory.sh query "<text>"` - similar translated entries from this repo's .po files (needs `AGENTIC_RETRIEVAL_ENGINE` or `retrieval` on PATH; see README.rst)

‎README.rst‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,44 @@ Skill 會將發現的問題分類為 ERROR、WARNING、INFO 並逐項列出,
450450
Skill 產生的譯文僅供輔助,送出 Pull Request 前請務必人工確認每一條翻譯是否\
451451
正確、是否符合\ `翻譯守則`_\ 。
452452

453+
翻譯記憶 (Translation Memory)
454+
------------------------------
455+
456+
``translate-po``\ 、\ ``check-terminology`` 與 ``validate-translation`` 會在翻譯\
457+
或校對時,從本專案既有的 po 檔中查詢相似的已翻譯條目與用詞的翻譯方式作為參考。\
458+
此功能使用 `agentic-retrieval <https://github.com/josix/agentic-retrieval>`_
459+
建立索引;未安裝時 Skill 會自動略過此步驟。查詢結果僅供參考,用詞仍以\
460+
`術語表 Glossary`_\ 為準。
461+
462+
事前準備(目前僅支援 macOS 與 Linux):
463+
464+
- 安裝 `uv <https://docs.astral.sh/uv/>`_\ 。
465+
- 取得 agentic-retrieval 的原始碼,並將環境變數指向其中的 ``engine`` 目錄:
466+
467+
.. code-block:: sh
468+
469+
export AGENTIC_RETRIEVAL_ENGINE=/path/to/agentic-retrieval/engine
470+
471+
- agentic-retrieval 正式發布後,也可以改用 ``uv tool install`` 安裝,讓\
472+
``retrieval`` 指令出現在 ``PATH`` 中,就不需要設定環境變數:
473+
474+
.. code-block:: sh
475+
476+
uv tool install "agentic-retrieval @ git+https://github.com/josix/agentic-retrieval.git#subdirectory=engine"
477+
478+
使用方式:
479+
480+
.. code-block:: sh
481+
482+
.scripts/translation_memory.sh index # 建立或更新索引
483+
.scripts/translation_memory.sh query "cache decorator" # 以英文查詢
484+
.scripts/translation_memory.sh query "快取 裝飾器" # 以中文查詢
485+
486+
索引存放在專案根目錄的 ``.agentic-retrieval/``\ (已列入 ``.gitignore``\ )。專案\
487+
檔案更新後(例如 ``git pull``\ ),下一次查詢會自動重建索引。預設只列出已翻譯且\
488+
未標記 fuzzy 的條目,可加上 ``--include-fuzzy`` 或 ``--include-untranslated``
489+
調整。
490+
453491
術語表 Glossary
454492
===============
455493

0 commit comments

Comments
 (0)