Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
- run: python tests/test_generated_views.py
- run: python tests/test_markdown_links.py
- run: python tests/test_locale_invariants.py
- run: python tests/test_locale_freshness_regressions.py
- name: Check localized source freshness
env:
BEFORE: ${{ github.event.before }}
Expand Down
15 changes: 11 additions & 4 deletions docs/localization/TRANSLATION_SURFACE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Translation Surface Inventory

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

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

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`.
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.

## Top-level files

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

| Path | Class | Treatment |
| --- | --- | --- |
| `docs/README.md` | A | Localized documentation index; route readers to localized operational guidance when a counterpart exists |
| `docs/ACQUISITION.md` | A | Full localized onboarding; commands/URLs unchanged |
| `docs/quickstart.md` | A | Full localized walkthrough; paths/identifiers unchanged |
| `docs/visitor_lobby_model.md` | A | Translate carefully; `visitor` has project-specific meaning |
Expand All @@ -44,10 +45,16 @@ For `zh-CN`, the first-release Class A route and selected Class B support surfac
| `docs/CONNECTOR_SAFE_WORDING.md` | B | Translate after core route, preserving compatibility intent |
| `docs/branding.md` | A | Translate explanation; product names remain unchanged |
| `docs/UPGRADING.md` | B | Translate as an upgrade companion after the core operational route is current |
| `docs/registry/*.md` | B | Translate as Registry Contract v1 reference companions; preserve fields, enums, paths, and examples structurally |
| `docs/WHY_LABNOTE.md` | B | Localized conceptual companion explaining how LabNote fits beside context files and model memory |
| `docs/PROJECT_PROVENANCE.md` | B | Localized project-provenance record |
| 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 |
| `docs/registry/REGISTRY_SURFACE_AUDIT.md` | D | Keep as a canonical historical pre-v1 audit; it is not current operational guidance |
| `docs/localization/GLOSSARY.md` | B | Maintain the canonical terminology source together with its explicitly mapped locale glossary |
| `docs/localization/TAG_DISPLAY_CATALOG.md` | B | Maintain canonical display text together with its explicitly mapped localized catalogue |
| other `docs/localization/` process files | D | Keep canonical unless a separate localization-maintenance need is established |
| `docs/localization/COMMUNICATION_REGISTRY_SOURCE_DRIFT.md` | D | Keep canonical as localization-maintenance history |
| `docs/localization/LOCALIZATION_CONTRACT.md` | D | Keep canonical as the maintainer-facing localization contract |
| `docs/localization/TRANSLATION_SURFACE.md` | D | Keep canonical as the maintainer-facing inventory |
| `docs/localization/ZH_CN_VALIDATION.md` | D | Keep canonical as the zh-CN validation record |

## `lobby/`

Expand Down
4 changes: 2 additions & 2 deletions locales/zh-CN/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

## 工作

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

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

## 了解项目

Expand Down
59 changes: 55 additions & 4 deletions tests/test_locale_freshness.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import hashlib
import json
import re
import subprocess
import sys
from pathlib import Path
Expand All @@ -26,17 +27,51 @@
}


def paired_surfaces() -> dict[str, str]:
pairs: dict[str, str] = {}
def source_for_locale(relative: str) -> str:
return SOURCE_OVERRIDES.get(relative, relative)


def locale_surfaces() -> dict[str, str]:
surfaces: dict[str, str] = {}
for locale_path in sorted(LOCALE_ROOT.rglob("*.md")):
locale_rel = locale_path.relative_to(ROOT).as_posix()
relative = locale_path.relative_to(LOCALE_ROOT).as_posix()
source_rel = SOURCE_OVERRIDES.get(relative, relative)
surfaces[locale_rel] = source_for_locale(relative)
return surfaces


def paired_surfaces() -> dict[str, str]:
pairs: dict[str, str] = {}
for locale_rel, source_rel in locale_surfaces().items():
if (ROOT / source_rel).is_file():
pairs[source_rel] = locale_rel
return pairs


def files_at(revision: str) -> set[str]:
result = subprocess.run(
["git", "ls-tree", "-r", "--name-only", revision],
cwd=ROOT,
check=True,
text=True,
capture_output=True,
)
return {line for line in result.stdout.splitlines() if line}


def paired_surfaces_at(revision: str) -> dict[str, str]:
files = files_at(revision)
pairs: dict[str, str] = {}
prefix = "locales/zh-CN/"
for locale_rel in sorted(
path for path in files if path.startswith(prefix) and path.endswith(".md")
):
source_rel = source_for_locale(locale_rel.removeprefix(prefix))
if source_rel in files:
pairs[source_rel] = locale_rel
return pairs


def digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()

Expand Down Expand Up @@ -64,7 +99,9 @@ def acknowledgements(pairs: dict[str, str]) -> dict[str, str]:
reason = entry.get("reason")
if not isinstance(source, str) or source not in pairs:
raise AssertionError(f"invalid acknowledgement source: {source!r}")
if not isinstance(source_sha256, str) or len(source_sha256) != 64:
if not isinstance(source_sha256, str) or not re.fullmatch(
r"[0-9a-f]{64}", source_sha256
):
raise AssertionError(f"invalid source_sha256 for {source!r}")
if not isinstance(reason, str) or not reason.strip():
raise AssertionError(f"missing review reason for {source!r}")
Expand All @@ -79,10 +116,24 @@ def main() -> int:
raise SystemExit("usage: test_locale_freshness.py <base> <head>")

pairs = paired_surfaces()
base_pairs = paired_surfaces_at(sys.argv[1])
changed = changed_paths(sys.argv[1], sys.argv[2])
reviewed = acknowledgements(pairs)
failures: list[str] = []

for locale_rel, source_rel in locale_surfaces().items():
if not (ROOT / source_rel).is_file():
failures.append(
f"localized surface has no canonical source: {locale_rel} ({source_rel})"
)

for source_rel, locale_rel in base_pairs.items():
if (ROOT / source_rel).is_file() and not (ROOT / locale_rel).is_file():
failures.append(
f"paired localized surface was removed while its canonical source remains: "
f"{locale_rel} ({source_rel})"
)

for source_rel, locale_rel in pairs.items():
if source_rel not in changed or locale_rel in changed:
continue
Expand Down
103 changes: 103 additions & 0 deletions tests/test_locale_freshness_regressions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Regression tests for locale-pair freshness and survival."""
from __future__ import annotations

import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path


ROOT = Path(__file__).resolve().parents[1]
CHECKER = ROOT / "tests" / "test_locale_freshness.py"


def git(repository: Path, *args: str) -> str:
result = subprocess.run(
["git", *args],
cwd=repository,
text=True,
capture_output=True,
check=False,
)
if result.returncode != 0:
raise AssertionError(f"git {' '.join(args)} failed:\n{result.stdout}\n{result.stderr}")
return result.stdout.strip()


def init_repository(repository: Path) -> str:
(repository / "tests").mkdir()
(repository / "locales" / "zh-CN").mkdir(parents=True)
shutil.copy2(CHECKER, repository / "tests" / CHECKER.name)
(repository / "tests" / "locale_review_acknowledgements.json").write_text(
json.dumps({"schema_version": 1, "acknowledgements": []}) + "\n",
encoding="utf-8",
)
(repository / "README.md").write_text("English source\n", encoding="utf-8")
(repository / "locales" / "zh-CN" / "README.md").write_text(
"Chinese counterpart\n", encoding="utf-8"
)
git(repository, "init", "--quiet")
git(repository, "config", "user.name", "Fixture")
git(repository, "config", "user.email", "fixture@example.invalid")
git(repository, "add", ".")
git(repository, "commit", "--quiet", "-m", "initial pair")
return git(repository, "rev-parse", "HEAD")


def commit(repository: Path, message: str) -> str:
git(repository, "add", "-A")
git(repository, "commit", "--quiet", "-m", message)
return git(repository, "rev-parse", "HEAD")


def run_checker(repository: Path, base: str, head: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, "tests/test_locale_freshness.py", base, head],
cwd=repository,
text=True,
capture_output=True,
check=False,
)


def expect_failure(result: subprocess.CompletedProcess[str], label: str) -> None:
if result.returncode == 0:
raise AssertionError(f"{label} was not rejected")


def main() -> int:
with tempfile.TemporaryDirectory() as temporary:
repository = Path(temporary)
base = init_repository(repository)
(repository / "locales" / "zh-CN" / "README.md").unlink()
head = commit(repository, "delete localized counterpart")
expect_failure(run_checker(repository, base, head), "localized deletion")

with tempfile.TemporaryDirectory() as temporary:
repository = Path(temporary)
base = init_repository(repository)
git(repository, "mv", "README.md", "GUIDE.md")
head = commit(repository, "rename canonical source only")
expect_failure(run_checker(repository, base, head), "unpaired source rename")

with tempfile.TemporaryDirectory() as temporary:
repository = Path(temporary)
base = init_repository(repository)
git(repository, "mv", "README.md", "GUIDE.md")
git(repository, "mv", "locales/zh-CN/README.md", "locales/zh-CN/GUIDE.md")
head = commit(repository, "rename both sides of pair")
result = run_checker(repository, base, head)
if result.returncode != 0:
raise AssertionError(
f"paired rename was rejected:\n{result.stdout}\n{result.stderr}"
)

print("locale freshness regression tests passed")
return 0


if __name__ == "__main__":
raise SystemExit(main())
52 changes: 52 additions & 0 deletions tests/test_locale_invariants.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@
ROOT = Path(__file__).resolve().parents[1]

SURFACES: dict[str, dict[str, object]] = {
"README.md": {
"locale": "locales/zh-CN/README.md",
"literals": (
"AI_ENTRYPOINT.md",
"lobby/",
"datadrops/",
"responses/",
"registry/",
"docs/ACQUISITION.md",
"docs/quickstart.md",
"https://github.com/Wonderforge-Lab/CapstanAI-LabNote/generate",
),
},
"AI_ENTRYPOINT.md": {
"locale": "locales/zh-CN/AI_ENTRYPOINT.md",
"literals": (
Expand All @@ -37,6 +50,45 @@
"bridge_config.json",
),
},
"docs/README.md": {
"locale": "locales/zh-CN/docs/README.md",
"literals": (
"ACQUISITION.md",
"quickstart.md",
"../lobby/ROUTINE_DEPOSIT_QUICKSTART.md",
"review_workflow.md",
"visitor_lobby_model.md",
"message_routing_model.md",
"REGISTRY_RECORDS.md",
"storage_policy.md",
"DOCUMENT_DEPOSIT_POLICY.md",
"../SECURITY.md",
"WHY_LABNOTE.md",
"PROJECT_PROVENANCE.md",
),
},
"docs/ACQUISITION.md": {
"locale": "locales/zh-CN/docs/ACQUISITION.md",
"literals": (
"AI_ENTRYPOINT.md",
"https://github.com/Wonderforge-Lab/CapstanAI-LabNote.git",
"git clone",
"git remote rename origin upstream",
"git remote add origin <YOUR-PRIVATE-REPO-URL>",
"git push -u origin main",
"Use this template",
),
},
"docs/quickstart.md": {
"locale": "locales/zh-CN/docs/quickstart.md",
"literals": (
"AI_ENTRYPOINT.md",
"ACQUISITION.md",
"registry/packets/<year>/",
"registry/responses/<year>/",
"accepted",
),
},
"docs/localization/GLOSSARY.md": {
"locale": "locales/zh-CN/GLOSSARY.md",
"literals": (
Expand Down
Loading