Skip to content

Commit fc985cd

Browse files
Close locale route and freshness gaps (#29)
Preserve localized pair coverage across deletions and renames, correct zh-CN front-door routes, and refresh the translation inventory.
2 parents a6d9351 + 2c08667 commit fc985cd

6 files changed

Lines changed: 224 additions & 10 deletions

File tree

‎.github/workflows/validate.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jobs:
2424
- run: python tests/test_generated_views.py
2525
- run: python tests/test_markdown_links.py
2626
- run: python tests/test_locale_invariants.py
27+
- run: python tests/test_locale_freshness_regressions.py
2728
- name: Check localized source freshness
2829
env:
2930
BEFORE: ${{ github.event.before }}

‎docs/localization/TRANSLATION_SURFACE.md‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Translation Surface Inventory
22

3-
Status: **implemented first-release inventory; Wave 4 source-alignment update in progress**
3+
Status: **current supported-locale inventory**
44
Canonical source language: English (`en`)
55
First supported locale: Simplified Chinese (`zh-CN`)
66

@@ -11,7 +11,7 @@ First supported locale: Simplified Chinese (`zh-CN`)
1111
- **C - keep canonical, localize around it:** machine/protocol structure should remain unchanged; translated explanation may be added elsewhere.
1212
- **D - defer:** historical, empty runtime, binary, or otherwise outside the first localization release.
1313

14-
For `zh-CN`, the first-release Class A route and selected Class B support surfaces were implemented and reviewed against their then-current English sources. Canonical changes merged after that review are explicitly tracked as Wave 4 source-alignment work; a class is a treatment decision, not a claim that its current English revision is already localized. Current locale support status is recorded in `locales/README.md`.
14+
For `zh-CN`, the Class A operational route and selected Class B support surfaces are implemented. A class is a treatment decision, not a claim that every current English document in that class has a localized counterpart. Current locale support status is recorded in `locales/README.md`; source/counterpart freshness is enforced for existing pairs.
1515

1616
## Top-level files
1717

@@ -30,6 +30,7 @@ For `zh-CN`, the first-release Class A route and selected Class B support surfac
3030

3131
| Path | Class | Treatment |
3232
| --- | --- | --- |
33+
| `docs/README.md` | A | Localized documentation index; route readers to localized operational guidance when a counterpart exists |
3334
| `docs/ACQUISITION.md` | A | Full localized onboarding; commands/URLs unchanged |
3435
| `docs/quickstart.md` | A | Full localized walkthrough; paths/identifiers unchanged |
3536
| `docs/visitor_lobby_model.md` | A | Translate carefully; `visitor` has project-specific meaning |
@@ -44,10 +45,16 @@ For `zh-CN`, the first-release Class A route and selected Class B support surfac
4445
| `docs/CONNECTOR_SAFE_WORDING.md` | B | Translate after core route, preserving compatibility intent |
4546
| `docs/branding.md` | A | Translate explanation; product names remain unchanged |
4647
| `docs/UPGRADING.md` | B | Translate as an upgrade companion after the core operational route is current |
47-
| `docs/registry/*.md` | B | Translate as Registry Contract v1 reference companions; preserve fields, enums, paths, and examples structurally |
48+
| `docs/WHY_LABNOTE.md` | B | Localized conceptual companion explaining how LabNote fits beside context files and model memory |
49+
| `docs/PROJECT_PROVENANCE.md` | B | Localized project-provenance record |
50+
| active Registry Contract v1 references under `docs/registry/` | B | Canonical English reference set; localized companions are deferred until a dedicated reference-translation need is established |
51+
| `docs/registry/REGISTRY_SURFACE_AUDIT.md` | D | Keep as a canonical historical pre-v1 audit; it is not current operational guidance |
4852
| `docs/localization/GLOSSARY.md` | B | Maintain the canonical terminology source together with its explicitly mapped locale glossary |
4953
| `docs/localization/TAG_DISPLAY_CATALOG.md` | B | Maintain canonical display text together with its explicitly mapped localized catalogue |
50-
| other `docs/localization/` process files | D | Keep canonical unless a separate localization-maintenance need is established |
54+
| `docs/localization/COMMUNICATION_REGISTRY_SOURCE_DRIFT.md` | D | Keep canonical as localization-maintenance history |
55+
| `docs/localization/LOCALIZATION_CONTRACT.md` | D | Keep canonical as the maintainer-facing localization contract |
56+
| `docs/localization/TRANSLATION_SURFACE.md` | D | Keep canonical as the maintainer-facing inventory |
57+
| `docs/localization/ZH_CN_VALIDATION.md` | D | Keep canonical as the zh-CN validation record |
5158

5259
## `lobby/`
5360

‎locales/zh-CN/docs/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
## 工作
1414

15-
- [进行常规投递](../../../lobby/ROUTINE_DEPOSIT_QUICKSTART.md) —— 以最小但有用的记录,把一项普通工作放到正确路径中。
15+
- [进行常规投递](../lobby/ROUTINE_DEPOSIT_QUICKSTART.md) —— 以最小但有用的记录,把一项普通工作放到正确路径中。
1616
- [理解审阅与决定](review_workflow.md) —— 查看回复如何成为经过审阅的结果。
1717
- [理解访客会话与会话身份](visitor_lobby_model.md) —— 了解实际工作区中的会话记录代表什么。
1818
- [理解消息路由](message_routing_model.md) —— 遵循会话或角色之间的消息模型。
@@ -22,7 +22,7 @@
2222

2323
- [阅读存储策略](storage_policy.md) —— 让台账保持精简、可检查,并符合工作区可见性的要求。
2424
- [阅读文档投递策略](DOCUMENT_DEPOSIT_POLICY.md) —— 处理文档和二进制文件,避免把 LabNote 变成普通文件堆。
25-
- [阅读安全说明](../../../SECURITY.md) —— 不要把凭证、token、密钥或无意暴露的私密材料放入错误的工作区。
25+
- [阅读安全说明](../SECURITY.md) —— 不要把凭证、token、密钥或无意暴露的私密材料放入错误的工作区。
2626

2727
## 了解项目
2828

‎tests/test_locale_freshness.py‎

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import hashlib
1212
import json
13+
import re
1314
import subprocess
1415
import sys
1516
from pathlib import Path
@@ -26,17 +27,51 @@
2627
}
2728

2829

29-
def paired_surfaces() -> dict[str, str]:
30-
pairs: dict[str, str] = {}
30+
def source_for_locale(relative: str) -> str:
31+
return SOURCE_OVERRIDES.get(relative, relative)
32+
33+
34+
def locale_surfaces() -> dict[str, str]:
35+
surfaces: dict[str, str] = {}
3136
for locale_path in sorted(LOCALE_ROOT.rglob("*.md")):
3237
locale_rel = locale_path.relative_to(ROOT).as_posix()
3338
relative = locale_path.relative_to(LOCALE_ROOT).as_posix()
34-
source_rel = SOURCE_OVERRIDES.get(relative, relative)
39+
surfaces[locale_rel] = source_for_locale(relative)
40+
return surfaces
41+
42+
43+
def paired_surfaces() -> dict[str, str]:
44+
pairs: dict[str, str] = {}
45+
for locale_rel, source_rel in locale_surfaces().items():
3546
if (ROOT / source_rel).is_file():
3647
pairs[source_rel] = locale_rel
3748
return pairs
3849

3950

51+
def files_at(revision: str) -> set[str]:
52+
result = subprocess.run(
53+
["git", "ls-tree", "-r", "--name-only", revision],
54+
cwd=ROOT,
55+
check=True,
56+
text=True,
57+
capture_output=True,
58+
)
59+
return {line for line in result.stdout.splitlines() if line}
60+
61+
62+
def paired_surfaces_at(revision: str) -> dict[str, str]:
63+
files = files_at(revision)
64+
pairs: dict[str, str] = {}
65+
prefix = "locales/zh-CN/"
66+
for locale_rel in sorted(
67+
path for path in files if path.startswith(prefix) and path.endswith(".md")
68+
):
69+
source_rel = source_for_locale(locale_rel.removeprefix(prefix))
70+
if source_rel in files:
71+
pairs[source_rel] = locale_rel
72+
return pairs
73+
74+
4075
def digest(path: Path) -> str:
4176
return hashlib.sha256(path.read_bytes()).hexdigest()
4277

@@ -64,7 +99,9 @@ def acknowledgements(pairs: dict[str, str]) -> dict[str, str]:
6499
reason = entry.get("reason")
65100
if not isinstance(source, str) or source not in pairs:
66101
raise AssertionError(f"invalid acknowledgement source: {source!r}")
67-
if not isinstance(source_sha256, str) or len(source_sha256) != 64:
102+
if not isinstance(source_sha256, str) or not re.fullmatch(
103+
r"[0-9a-f]{64}", source_sha256
104+
):
68105
raise AssertionError(f"invalid source_sha256 for {source!r}")
69106
if not isinstance(reason, str) or not reason.strip():
70107
raise AssertionError(f"missing review reason for {source!r}")
@@ -79,10 +116,24 @@ def main() -> int:
79116
raise SystemExit("usage: test_locale_freshness.py <base> <head>")
80117

81118
pairs = paired_surfaces()
119+
base_pairs = paired_surfaces_at(sys.argv[1])
82120
changed = changed_paths(sys.argv[1], sys.argv[2])
83121
reviewed = acknowledgements(pairs)
84122
failures: list[str] = []
85123

124+
for locale_rel, source_rel in locale_surfaces().items():
125+
if not (ROOT / source_rel).is_file():
126+
failures.append(
127+
f"localized surface has no canonical source: {locale_rel} ({source_rel})"
128+
)
129+
130+
for source_rel, locale_rel in base_pairs.items():
131+
if (ROOT / source_rel).is_file() and not (ROOT / locale_rel).is_file():
132+
failures.append(
133+
f"paired localized surface was removed while its canonical source remains: "
134+
f"{locale_rel} ({source_rel})"
135+
)
136+
86137
for source_rel, locale_rel in pairs.items():
87138
if source_rel not in changed or locale_rel in changed:
88139
continue
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
#!/usr/bin/env python3
2+
"""Regression tests for locale-pair freshness and survival."""
3+
from __future__ import annotations
4+
5+
import json
6+
import shutil
7+
import subprocess
8+
import sys
9+
import tempfile
10+
from pathlib import Path
11+
12+
13+
ROOT = Path(__file__).resolve().parents[1]
14+
CHECKER = ROOT / "tests" / "test_locale_freshness.py"
15+
16+
17+
def git(repository: Path, *args: str) -> str:
18+
result = subprocess.run(
19+
["git", *args],
20+
cwd=repository,
21+
text=True,
22+
capture_output=True,
23+
check=False,
24+
)
25+
if result.returncode != 0:
26+
raise AssertionError(f"git {' '.join(args)} failed:\n{result.stdout}\n{result.stderr}")
27+
return result.stdout.strip()
28+
29+
30+
def init_repository(repository: Path) -> str:
31+
(repository / "tests").mkdir()
32+
(repository / "locales" / "zh-CN").mkdir(parents=True)
33+
shutil.copy2(CHECKER, repository / "tests" / CHECKER.name)
34+
(repository / "tests" / "locale_review_acknowledgements.json").write_text(
35+
json.dumps({"schema_version": 1, "acknowledgements": []}) + "\n",
36+
encoding="utf-8",
37+
)
38+
(repository / "README.md").write_text("English source\n", encoding="utf-8")
39+
(repository / "locales" / "zh-CN" / "README.md").write_text(
40+
"Chinese counterpart\n", encoding="utf-8"
41+
)
42+
git(repository, "init", "--quiet")
43+
git(repository, "config", "user.name", "Fixture")
44+
git(repository, "config", "user.email", "fixture@example.invalid")
45+
git(repository, "add", ".")
46+
git(repository, "commit", "--quiet", "-m", "initial pair")
47+
return git(repository, "rev-parse", "HEAD")
48+
49+
50+
def commit(repository: Path, message: str) -> str:
51+
git(repository, "add", "-A")
52+
git(repository, "commit", "--quiet", "-m", message)
53+
return git(repository, "rev-parse", "HEAD")
54+
55+
56+
def run_checker(repository: Path, base: str, head: str) -> subprocess.CompletedProcess[str]:
57+
return subprocess.run(
58+
[sys.executable, "tests/test_locale_freshness.py", base, head],
59+
cwd=repository,
60+
text=True,
61+
capture_output=True,
62+
check=False,
63+
)
64+
65+
66+
def expect_failure(result: subprocess.CompletedProcess[str], label: str) -> None:
67+
if result.returncode == 0:
68+
raise AssertionError(f"{label} was not rejected")
69+
70+
71+
def main() -> int:
72+
with tempfile.TemporaryDirectory() as temporary:
73+
repository = Path(temporary)
74+
base = init_repository(repository)
75+
(repository / "locales" / "zh-CN" / "README.md").unlink()
76+
head = commit(repository, "delete localized counterpart")
77+
expect_failure(run_checker(repository, base, head), "localized deletion")
78+
79+
with tempfile.TemporaryDirectory() as temporary:
80+
repository = Path(temporary)
81+
base = init_repository(repository)
82+
git(repository, "mv", "README.md", "GUIDE.md")
83+
head = commit(repository, "rename canonical source only")
84+
expect_failure(run_checker(repository, base, head), "unpaired source rename")
85+
86+
with tempfile.TemporaryDirectory() as temporary:
87+
repository = Path(temporary)
88+
base = init_repository(repository)
89+
git(repository, "mv", "README.md", "GUIDE.md")
90+
git(repository, "mv", "locales/zh-CN/README.md", "locales/zh-CN/GUIDE.md")
91+
head = commit(repository, "rename both sides of pair")
92+
result = run_checker(repository, base, head)
93+
if result.returncode != 0:
94+
raise AssertionError(
95+
f"paired rename was rejected:\n{result.stdout}\n{result.stderr}"
96+
)
97+
98+
print("locale freshness regression tests passed")
99+
return 0
100+
101+
102+
if __name__ == "__main__":
103+
raise SystemExit(main())

‎tests/test_locale_invariants.py‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@
1313
ROOT = Path(__file__).resolve().parents[1]
1414

1515
SURFACES: dict[str, dict[str, object]] = {
16+
"README.md": {
17+
"locale": "locales/zh-CN/README.md",
18+
"literals": (
19+
"AI_ENTRYPOINT.md",
20+
"lobby/",
21+
"datadrops/",
22+
"responses/",
23+
"registry/",
24+
"docs/ACQUISITION.md",
25+
"docs/quickstart.md",
26+
"https://github.com/Wonderforge-Lab/CapstanAI-LabNote/generate",
27+
),
28+
},
1629
"AI_ENTRYPOINT.md": {
1730
"locale": "locales/zh-CN/AI_ENTRYPOINT.md",
1831
"literals": (
@@ -37,6 +50,45 @@
3750
"bridge_config.json",
3851
),
3952
},
53+
"docs/README.md": {
54+
"locale": "locales/zh-CN/docs/README.md",
55+
"literals": (
56+
"ACQUISITION.md",
57+
"quickstart.md",
58+
"../lobby/ROUTINE_DEPOSIT_QUICKSTART.md",
59+
"review_workflow.md",
60+
"visitor_lobby_model.md",
61+
"message_routing_model.md",
62+
"REGISTRY_RECORDS.md",
63+
"storage_policy.md",
64+
"DOCUMENT_DEPOSIT_POLICY.md",
65+
"../SECURITY.md",
66+
"WHY_LABNOTE.md",
67+
"PROJECT_PROVENANCE.md",
68+
),
69+
},
70+
"docs/ACQUISITION.md": {
71+
"locale": "locales/zh-CN/docs/ACQUISITION.md",
72+
"literals": (
73+
"AI_ENTRYPOINT.md",
74+
"https://github.com/Wonderforge-Lab/CapstanAI-LabNote.git",
75+
"git clone",
76+
"git remote rename origin upstream",
77+
"git remote add origin <YOUR-PRIVATE-REPO-URL>",
78+
"git push -u origin main",
79+
"Use this template",
80+
),
81+
},
82+
"docs/quickstart.md": {
83+
"locale": "locales/zh-CN/docs/quickstart.md",
84+
"literals": (
85+
"AI_ENTRYPOINT.md",
86+
"ACQUISITION.md",
87+
"registry/packets/<year>/",
88+
"registry/responses/<year>/",
89+
"accepted",
90+
),
91+
},
4092
"docs/localization/GLOSSARY.md": {
4193
"locale": "locales/zh-CN/GLOSSARY.md",
4294
"literals": (

0 commit comments

Comments
 (0)