Skip to content
Open
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
8 changes: 8 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ self-hosted-runner:
- crabbox
- openclaw
- docs

paths:
.github/workflows/r2-pages.yml:
ignore:
# GitHub added concurrency `queue: max` on 2026-05-07, but actionlint
# 1.7.11 has not learned the key yet. Keep this scoped to R2 Pages.
# https://github.blog/changelog/2026-05-07-github-actions-concurrency-groups-now-allow-larger-queues/
- 'unexpected key "queue" for "concurrency" section'
14 changes: 11 additions & 3 deletions .github/scripts/i18n/commit_locale_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,20 @@ def ensure_base_current(base_source_sha: str, locale: str) -> bool:

def has_locale_changes(locale: str) -> bool:
result = run(
["git", "status", "--porcelain", "--untracked-files=all", "--", f"docs/{locale}", f"docs/.i18n/{locale}.tm.jsonl"],
["git", "status", "--porcelain", "--untracked-files=all", "--", *locale_pathspecs(locale)],
check=True,
)
return bool(result.stdout.strip())


def locale_pathspecs(locale: str) -> list[str]:
tm_path = f"docs/.i18n/{locale}.tm.jsonl"
paths = [f"docs/{locale}"]
if Path(tm_path).exists() or run(["git", "ls-files", "--error-unmatch", tm_path], check=False).returncode == 0:
paths.append(tm_path)
return paths


def pending_allowed(locale: str, locale_slug: str, shard_index: str, shard_total: str) -> set[str]:
pending_file = Path(".openclaw-sync") / f"docs-i18n-{locale_slug}-s{shard_index}of{shard_total}.txt"
allowed = {f"docs/.i18n/{locale}.tm.jsonl"}
Expand Down Expand Up @@ -122,7 +130,7 @@ def artifact_allowed(locale: str, artifact_dir: str) -> set[str]:


def enforce_canary_scope(locale: str, allowed: set[str]) -> None:
status = git_stdout(["status", "--porcelain", "--untracked-files=all", "--", f"docs/{locale}", f"docs/.i18n/{locale}.tm.jsonl"])
status = git_stdout(["status", "--porcelain", "--untracked-files=all", "--", *locale_pathspecs(locale)])
changed = {line[3:] for line in status.splitlines() if line.strip()}
bad = sorted(path for path in changed if path not in allowed)
if bad:
Expand Down Expand Up @@ -153,7 +161,7 @@ def commit_locale(

git_stdout(["config", "user.name", "openclaw-docs-i18n[bot]"])
git_stdout(["config", "user.email", "openclaw-docs-i18n[bot]@users.noreply.github.com"])
git_stdout(["add", f"docs/{locale}", f"docs/.i18n/{locale}.tm.jsonl"])
git_stdout(["add", *locale_pathspecs(locale)])
git_stdout(["commit", "-m", f"chore(i18n): refresh {locale} translations"])

for attempt in range(1, attempts + 1):
Expand Down
103 changes: 89 additions & 14 deletions .github/scripts/i18n/plan_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@
Parameters:
--target-locale: Locale slug/name to rerun, or all. Default: TARGET_LOCALE/all.
--batch-size: Maximum locales per follow-up batch. Default: 4.
--docs-root: Docs directory used to size full-translation shards. Default: docs.
--target-docs-per-shard: Desired source documents per shard. Default: 250.
--max-shards: Maximum shards per locale. Default: 4.

Outputs:
GITHUB_OUTPUT receives locale_count, canary_locale, canary_locale_slug,
selected_locales, and batch_1 through batch_6 JSON matrices. The step summary
records the selected canary and batch count. Exits non-zero for unknown
locales or oversized batch requests.
GITHUB_OUTPUT receives locale_count, canary locale fields, selected_locales,
shard_total, and batch_1 through batch_6 JSON matrices. Each batch exposes a
shard matrix for translation jobs and a locale matrix for one finalizer per
locale. The step summary records the selected canary and batch count. Exits
non-zero for unknown locales or oversized batch requests.

Examples:
python .github/scripts/i18n/plan_full.py --target-locale all
Expand All @@ -25,13 +29,18 @@

import argparse
import json
import math
import os
import re
from dataclasses import dataclass
from pathlib import Path


MAX_BATCHES = 6
DEFAULT_BATCH_SIZE = 4
DEFAULT_TARGET_DOCS_PER_SHARD = 250
DEFAULT_MAX_SHARDS = 4
LOCALE_DIR_RE = re.compile(r"^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})?$")
LOCALES: tuple[tuple[str, str], ...] = (
("zh-CN", "zh-cn"),
("zh-TW", "zh-tw"),
Expand Down Expand Up @@ -64,6 +73,53 @@ class Locale:
def matrix_item(self) -> dict[str, str]:
return {"locale": self.locale, "locale_slug": self.locale_slug}

def matrix_item_with_shards(self, shard_total: int) -> dict[str, str]:
item = self.matrix_item()
item["shard_total"] = str(shard_total)
return item


def is_locale_dir(path: Path) -> bool:
return path.is_dir() and LOCALE_DIR_RE.match(path.name) is not None and (path / ".i18n" / "README.md").exists()


def source_doc_count(docs_root: Path) -> int:
locale_dirs = {path.name for path in docs_root.iterdir() if is_locale_dir(path)}
count = 0
for path in docs_root.rglob("*"):
if not path.is_file() or path.suffix.lower() not in {".md", ".mdx"}:
continue
rel = path.relative_to(docs_root)
parts = rel.parts
if parts and parts[0] in locale_dirs:
continue
rel_posix = rel.as_posix()
if rel_posix.startswith(".i18n/") or rel_posix.startswith(".generated/"):
continue
count += 1
return count


def shard_total_for_doc_count(doc_count: int, target_docs_per_shard: int, max_shards: int) -> int:
if target_docs_per_shard < 1:
raise SystemExit(f"invalid target docs per shard: {target_docs_per_shard}")
if max_shards < 1:
raise SystemExit(f"invalid max shards: {max_shards}")
return max(1, min(max_shards, math.ceil(doc_count / target_docs_per_shard)))


def expand_shards(batch: list[Locale], shard_total: int) -> list[dict[str, str]]:
return [
{
"locale": locale.locale,
"locale_slug": locale.locale_slug,
"shard_index": str(shard_index),
"shard_total": str(shard_total),
}
for locale in batch
for shard_index in range(shard_total)
]


def all_locales() -> list[Locale]:
return [Locale(locale, slug) for locale, slug in LOCALES]
Expand Down Expand Up @@ -94,11 +150,11 @@ def build_batches(selected: list[Locale], batch_size: int) -> list[list[Locale]]
return batches


def matrix_json(locales: list[Locale]) -> str:
return json.dumps({"include": [locale.matrix_item() for locale in locales]}, separators=(",", ":"))
def matrix_json(items: list[dict[str, str]]) -> str:
return json.dumps({"include": items}, separators=(",", ":"))


def append_outputs(selected: list[Locale], batches: list[list[Locale]]) -> None:
def append_outputs(selected: list[Locale], batches: list[list[Locale]], shard_total: int, source_docs: int) -> None:
output = os.environ.get("GITHUB_OUTPUT")
if not output:
return
Expand All @@ -108,13 +164,16 @@ def append_outputs(selected: list[Locale], batches: list[list[Locale]]) -> None:
fh.write(f"canary_locale={canary.locale}\n")
fh.write(f"canary_locale_slug={canary.locale_slug}\n")
fh.write(f"selected_locales={','.join(locale.locale for locale in selected)}\n")
fh.write(f"source_doc_count={source_docs}\n")
fh.write(f"shard_total={shard_total}\n")
for index in range(MAX_BATCHES):
batch = batches[index] if index < len(batches) else []
fh.write(f"batch_{index + 1}_count={len(batch)}\n")
fh.write(f"batch_{index + 1}={matrix_json(batch)}\n")
fh.write(f"batch_{index + 1}={matrix_json(expand_shards(batch, shard_total))}\n")
fh.write(f"batch_{index + 1}_locales={matrix_json([locale.matrix_item_with_shards(shard_total) for locale in batch])}\n")


def append_summary(target_locale: str, selected: list[Locale], batches: list[list[Locale]]) -> None:
def append_summary(target_locale: str, selected: list[Locale], batches: list[list[Locale]], shard_total: int, source_docs: int) -> None:
summary = os.environ.get("GITHUB_STEP_SUMMARY")
if not summary:
return
Expand All @@ -123,19 +182,32 @@ def append_summary(target_locale: str, selected: list[Locale], batches: list[lis
fh.write(f"- requested target: `{normalize_target(target_locale)}`\n")
fh.write(f"- selected locales: `{', '.join(locale.locale for locale in selected)}`\n")
fh.write(f"- canary locale: `{selected[0].locale}`\n")
fh.write(f"- source docs: `{source_docs}`\n")
fh.write(f"- shards per locale: `{shard_total}`\n")
for index, batch in enumerate(batches, start=1):
fh.write(f"- batch {index}: `{', '.join(locale.locale for locale in batch)}`\n")


def plan_full(target_locale: str, batch_size: int) -> dict[str, object]:
def plan_full(
target_locale: str,
batch_size: int,
docs_root: Path | None = None,
target_docs_per_shard: int = DEFAULT_TARGET_DOCS_PER_SHARD,
max_shards: int = DEFAULT_MAX_SHARDS,
) -> dict[str, object]:
selected = select_locales(target_locale)
batches = build_batches(selected, batch_size)
append_outputs(selected, batches)
append_summary(target_locale, selected, batches)
source_docs = source_doc_count(docs_root or Path("docs"))
shard_total = shard_total_for_doc_count(source_docs, target_docs_per_shard, max_shards)
append_outputs(selected, batches, shard_total, source_docs)
append_summary(target_locale, selected, batches, shard_total, source_docs)
return {
"selected": [locale.matrix_item() for locale in selected],
"canary": selected[0].matrix_item(),
"batches": [[locale.matrix_item() for locale in batch] for batch in batches],
"source_doc_count": source_docs,
"shard_total": shard_total,
"batches": [expand_shards(batch, shard_total) for batch in batches],
"batch_locales": [[locale.matrix_item_with_shards(shard_total) for locale in batch] for batch in batches],
}


Expand All @@ -153,12 +225,15 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument("--target-locale", default=os.environ.get("TARGET_LOCALE", "all"))
parser.add_argument("--batch-size", default=DEFAULT_BATCH_SIZE, type=int)
parser.add_argument("--docs-root", default="docs", type=Path)
parser.add_argument("--target-docs-per-shard", default=DEFAULT_TARGET_DOCS_PER_SHARD, type=int)
parser.add_argument("--max-shards", default=DEFAULT_MAX_SHARDS, type=int)
return parser.parse_args()


def main() -> None:
args = parse_args()
plan = plan_full(args.target_locale, args.batch_size)
plan = plan_full(args.target_locale, args.batch_size, args.docs_root, args.target_docs_per_shard, args.max_shards)
print(json.dumps(plan, indent=2, sort_keys=True))


Expand Down
17 changes: 14 additions & 3 deletions .github/scripts/i18n/summarize_full.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,20 @@ def read_artifacts(artifacts_root: Path) -> tuple[dict[str, dict[str, object]],
metadata_only_count += 1
if artifact_role == "canary":
continue
existing = artifacts.get(locale)
if existing is None or (not existing.get("failed_reason") and failed_reason):
artifacts[locale] = metadata
existing = artifacts.setdefault(
locale,
{
"locale": locale,
"changed_count": 0,
"deleted_count": 0,
"failed_reason": "",
},
)
existing["changed_count"] = int(existing.get("changed_count") or 0) + changed_count
existing["deleted_count"] = int(existing.get("deleted_count") or 0) + deleted_count
if failed_reason:
previous = str(existing.get("failed_reason") or "")
existing["failed_reason"] = "; ".join(part for part in [previous, failed_reason] if part)
return artifacts, invalid, artifact_count, metadata_only_count


Expand Down
Loading