Files
claude-howto/scripts/sync_translations.py
Luong NGUYEN 699fb39a46 ci: shift-left quality gates — add mypy to pre-commit, fix CI failures (#53)
* ci: shift-left quality gates — add mypy to pre-commit, fix CI failures

- Add mypy pre-commit hook (mirrors-mypy v1.13.0) so type checks run locally
- Add [tool.mypy] config to scripts/pyproject.toml with overrides for untyped libs (ebooklib, sync_translations)
- Add mypy>=1.8.0 to requirements-dev.txt
- Fix CI test.yml: remove continue-on-error: true from lint/security/type-check jobs (was silently swallowing failures)
- Fix CI bandit -c path: pyproject.toml → scripts/pyproject.toml
- Fix CI mypy command: use --config-file scripts/pyproject.toml
- Fix CI build-epub: add type-check to needs, fix if: success() → !failure() && !cancelled()
- Fix ruff errors in sync_translations.py (RUF013 implicit Optional, SIM102 nested if)
- Fix mypy errors: add list[str] annotations to errors vars in check_cross_references.py and check_links.py

* fix(ci): install mmdc in build-epub job and correct return type annotation

- Add npm install step for @mermaid-js/mermaid-cli before Build EPUB
  to fix CI failure (mmdc not found error)
- Fix check_translation_status() return type from list[dict] to
  tuple[list[dict], list[dict]] to match the actual return value

* fix(ci): pass --no-sandbox to Puppeteer in build-epub CI job

mmdc (Mermaid CLI) uses Puppeteer/Chromium which requires --no-sandbox
in the GitHub Actions sandboxed environment. Add --puppeteer-config flag
to build_epub.py that passes a Puppeteer JSON config file to mmdc via -p,
and use it in the CI workflow to inject the no-sandbox args.
2026-04-07 00:51:44 +02:00

257 lines
7.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Detect outdated Vietnamese translations compared to English version.
This script compares modification times between English and Vietnamese
documentation files to identify which translations need updating.
Usage:
python scripts/sync_translations.py
python scripts/sync_translations.py --verbose
"""
import argparse
from datetime import datetime
from pathlib import Path
def check_translation_status(
root_path: Path | None = None, verbose: bool = False
) -> tuple[list[dict], list[dict]]:
"""
Compare modification times between English and Vietnamese files.
Args:
root_path: Root directory of the repository (default: script parent parent)
verbose: Print detailed information
Returns:
List of outdated files with metadata
"""
if root_path is None:
root_path = Path(__file__).parent.parent
# Get all English markdown files (excluding vi/ directory)
en_files = [
f
for f in root_path.rglob("*.md")
if "vi/" not in str(f) and ".claude" not in str(f)
]
# Get all Vietnamese markdown files
vi_dir = root_path / "vi"
vi_files = list(vi_dir.rglob("*.md")) if vi_dir.exists() else []
# Build modification time mapping
en_mtime = {f: f.stat().st_mtime for f in en_files}
vi_mtime = {f: f.stat().st_mtime for f in vi_files}
outdated = []
not_translated = []
for en_file, en_time in sorted(en_mtime.items()):
# Find corresponding Vietnamese file
try:
rel_path = en_file.relative_to(root_path)
except ValueError:
# File is not relative to root (shouldn't happen)
if verbose:
print(f"⚠️ Skipping non-relative file: {en_file}")
continue
vi_file = root_path / "vi" / rel_path
if vi_file in vi_mtime:
vi_time = vi_mtime[vi_file]
if en_time > vi_time:
outdated.append(
{
"file": rel_path,
"en_mtime": datetime.fromtimestamp(en_time),
"vi_mtime": datetime.fromtimestamp(vi_time),
"days_diff": (en_time - vi_time) / 86400, # Convert to days
}
)
else:
not_translated.append(
{
"file": rel_path,
"status": "NOT_TRANSLATED",
}
)
# Sort outdated by days difference (most outdated first)
outdated.sort(key=lambda x: x["days_diff"], reverse=True)
return outdated, not_translated
def format_outdated_table(outdated: list[dict]) -> str:
"""Format outdated files as a Markdown table."""
if not outdated:
return "✅ **No outdated translations found!** All files are up to date.\n"
table = "### 🕰️ Outdated Translations (Need Update)\n\n"
table += "| File | Last EN Update | Last VI Update | Days Behind |\n"
table += "|------|----------------|----------------|-------------|\n"
for item in outdated:
file_path = str(item["file"])
en_date = item["en_mtime"].strftime("%Y-%m-%d")
vi_date = item["vi_mtime"].strftime("%Y-%m-%d")
days = int(item["days_diff"])
# Truncate long paths for display
if len(file_path) > 50:
file_path = "..." + file_path[-47:]
table += f"| `{file_path}` | {en_date} | {vi_date} | 🔴 **{days} days** |\n"
return table
def format_not_translated_table(not_translated: list[dict]) -> str:
"""Format not translated files as a Markdown table."""
if not not_translated:
return "\n✅ **All files have been translated!**\n"
table = "\n### 📝 Not Translated Yet\n\n"
table += "| File | Status |\n"
table += "|------|--------|\n"
for item in not_translated:
file_path = str(item["file"])
# Truncate long paths for display
if len(file_path) > 60:
file_path = "..." + file_path[-57:]
table += f"| `{file_path}` | ⏳ **Not translated** |\n"
return table
def format_summary(outdated: list[dict], not_translated: list[dict]) -> str:
"""Format summary statistics."""
total_outdated = len(outdated)
total_not_translated = len(not_translated)
total_files = total_outdated + total_not_translated
summary = "## 📊 Summary\n\n"
summary += f"- **Total files needing attention:** {total_files}\n"
summary += f"- **Outdated translations:** {total_outdated}\n"
summary += f"- **Not translated yet:** {total_not_translated}\n"
if total_outdated > 0:
most_outdated = max(outdated, key=lambda x: x["days_diff"])
summary += f"- **Most outdated file:** {most_outdated['file']} ({int(most_outdated['days_diff'])} days behind)\n"
summary += "\n---\n\n"
return summary
def update_translation_queue(
root_path: Path, outdated: list[dict], not_translated: list[dict]
) -> None:
"""
Update vi/TRANSLATION_QUEUE.md with current status.
Note: This is a placeholder for future implementation.
Currently, the queue is manually maintained.
"""
pass
def main():
parser = argparse.ArgumentParser(
description="Check Vietnamese translation status against English version"
)
parser.add_argument(
"--verbose", "-v", action="store_true", help="Print detailed information"
)
parser.add_argument(
"--root",
"-r",
type=Path,
default=None,
help="Root directory of repository (default: auto-detect)",
)
parser.add_argument(
"--update-queue",
action="store_true",
help="Update TRANSLATION_QUEUE.md with current status (experimental)",
)
args = parser.parse_args()
# Detect root path if not provided
root_path = args.root or Path(__file__).parent.parent
if args.verbose:
print(f"🔍 Checking translations in: {root_path}")
print()
# Check translation status
outdated, not_translated = check_translation_status(root_path, args.verbose)
# Print summary to console
print("=" * 60)
print("🌏 Vietnamese Translation Status Report")
print("=" * 60)
print()
total_outdated = len(outdated)
total_not_translated = len(not_translated)
if total_outdated == 0 and total_not_translated == 0:
print("✅ **Congratulations!** All files are up to date.")
print()
return
print(
f"📊 Found {total_outdated} outdated + {total_not_translated} not translated files"
)
print()
if args.verbose and outdated:
print("🕰️ OUTDATED FILES (need update):")
print("-" * 60)
for i, item in enumerate(outdated, 1):
print(f"{i}. {item['file']}")
print(f" EN: {item['en_mtime'].strftime('%Y-%m-%d %H:%M')}")
print(f" VI: {item['vi_mtime'].strftime('%Y-%m-%d %H:%M')}")
print(f" Behind by: {int(item['days_diff'])} days")
print()
if args.verbose and not_translated:
print("📝 NOT TRANSLATED FILES:")
print("-" * 60)
for i, item in enumerate(not_translated[:20], 1): # Limit to 20
print(f"{i}. {item['file']}")
if len(not_translated) > 20:
print(f"... and {len(not_translated) - 20} more files")
print()
# Print Markdown-formatted report
print("=" * 60)
print("📄 Markdown Report (copy to TRANSLATION_QUEUE.md)")
print("=" * 60)
print()
report = format_summary(outdated, not_translated)
report += format_outdated_table(outdated)
report += format_not_translated_table(not_translated)
print(report)
# Optionally update TRANSLATION_QUEUE.md
if args.update_queue and args.verbose:
print("⚠️ --update-queue is experimental and not yet implemented")
print(" Please manually update TRANSLATION_QUEUE.md")
if __name__ == "__main__":
main()