Skip to content

Commit 78f7a6c

Browse files
authored
[CI] Fix doc translate workflow (vllm-project#11597)
Fix doc translate workflow error - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@1f486d9 Signed-off-by: wangxiyuan <wangxiyuan1007@gmail.com>
1 parent c2e6f26 commit 78f7a6c

3 files changed

Lines changed: 345 additions & 105 deletions

File tree

.github/workflows/schedule_doc_translate.yaml

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ on:
2727
required: false
2828
default: 'main'
2929
type: string
30+
force:
31+
description: 'Force full regeneration of ALL .po files (discard existing translations)'
32+
required: false
33+
default: false
34+
type: boolean
3035

3136
concurrency:
3237
group: translation-${{ github.ref }}
@@ -38,15 +43,12 @@ jobs:
3843
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
3944
permissions:
4045
contents: read
41-
env:
42-
UPSTREAM_REPO: vllm-project/vllm-ascend
4346
steps:
4447
- name: Checkout repository
4548
uses: actions/checkout@v7
4649
with:
47-
repository: vllm-ascend-ci/vllm-ascend
4850
token: ${{ secrets.PAT_TOKEN }}
49-
ref: main
51+
ref: ${{ inputs.target_branch || 'main' }}
5052

5153
- name: Setup git and branch
5254
run: |
@@ -55,17 +57,10 @@ jobs:
5557
BRANCH_NAME="auto-pr/doc-translate-$(date +%Y%m%d%H%M%S)"
5658
echo "BRANCH_NAME=${BRANCH_NAME}" >> $GITHUB_ENV
5759
58-
git remote add upstream "https://github.com/${{ env.UPSTREAM_REPO }}.git"
59-
git fetch upstream
6060
git config user.name "${{ github.actor }}"
6161
git config user.email "${{ github.actor }}@users.noreply.github.com"
62-
git checkout -B "${BRANCH_NAME}" "upstream/${TARGET_BRANCH}"
63-
64-
# Use latest translation scripts from upstream/main
65-
git checkout upstream/main -- .github/workflows/scripts/po_translate.py
66-
git checkout upstream/main -- .github/workflows/scripts/detect_po_changes.py
67-
git checkout upstream/main -- docs/requirements-docs.txt
68-
git restore --staged .github/workflows/scripts/po_translate.py .github/workflows/scripts/detect_po_changes.py docs/requirements-docs.txt
62+
git checkout -B "${BRANCH_NAME}"
63+
git remote add fork "https://github.com/vllm-ascend-ci/vllm-ascend.git"
6964
7065
- name: Setup Python
7166
uses: actions/setup-python@v6
@@ -77,7 +72,9 @@ jobs:
7772
run: |
7873
pip install -r requirements-docs.txt
7974
cd ..
80-
python .github/workflows/scripts/detect_po_changes.py --output-json /tmp/po_changes.json
75+
FORCE_FLAG=""
76+
if [ "${{ inputs.force }}" = "true" ]; then FORCE_FLAG="--force"; fi
77+
python .github/workflows/scripts/detect_po_changes.py --output-json /tmp/po_changes.json $FORCE_FLAG
8178
8279
- name: Detect PO files to translate
8380
id: detect
@@ -102,15 +99,24 @@ jobs:
10299
103100
- name: Translate PO files
104101
if: steps.detect.outputs.has_changes == 'true'
102+
id: translate
105103
env:
106104
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
107105
run: |
108106
python .github/workflows/scripts/po_translate.py \
109107
--files "${{ steps.detect.outputs.files }}" \
110108
--output-json /tmp/translation_results.json
111109
110+
- name: Validate translation coverage
111+
if: steps.translate.outcome == 'success'
112+
id: validate
113+
run: |
114+
python .github/workflows/scripts/po_translate.py \
115+
--files "${{ steps.detect.outputs.files }}" \
116+
--validate-only
117+
112118
- name: Process translated files
113-
if: steps.detect.outputs.has_changes == 'true'
119+
if: steps.validate.outcome == 'success'
114120
id: results
115121
run: |
116122
[ ! -f /tmp/translation_results.json ] && echo "No results" && exit 1
@@ -144,22 +150,22 @@ jobs:
144150
} >> $GITHUB_OUTPUT
145151
146152
- name: Generate Chinese docs
147-
if: steps.detect.outputs.has_changes == 'true'
153+
if: steps.validate.outcome == 'success'
148154
run: |
149155
python tools/generate_zh_docs.py
150156
151157
- name: Commit and push
152-
if: steps.detect.outputs.has_changes == 'true'
158+
if: steps.validate.outcome == 'success'
153159
env:
154160
GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }}
155161
run: |
156162
git diff --cached --quiet && echo "Nothing to commit" && exit 1
157163
count=$(git diff --cached --name-only | wc -l)
158164
git commit -s -m "[Doc] Auto-translate ${count} file(s)"
159-
git push -f origin "${{ env.BRANCH_NAME }}"
165+
git push -f fork "${{ env.BRANCH_NAME }}"
160166
161167
- name: Create PR in upstream
162-
if: steps.detect.outputs.has_changes == 'true'
168+
if: steps.validate.outcome == 'success'
163169
uses: actions/github-script@v9
164170
env:
165171
FILE_LIST: ${{ steps.results.outputs.file_list }}

.github/workflows/scripts/detect_po_changes.py

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@
3939
FENCE_RE = __import__("re").compile(r"^(`{3,}|~{3,})")
4040
COMMENT_RE = __import__("re").compile(r"^<!--.*-->$")
4141

42+
# MkDocs Material extensions that should be recognized but whose
43+
# translatable content is extracted as separate paragraphs.
44+
# - !!! type ["title"] → admonition (note, warning, tip, etc.)
45+
# - ??? ["title"] → collapsible/details
46+
# - === "tab label" → content tabs
47+
ADMONITION_RE = __import__("re").compile(r'^!!!\s+\w+(\s+"[^"]*")?\s*$')
48+
DETAILS_RE = __import__("re").compile(r'^\?\?\?(\s+"[^"]*")?\s*$')
49+
TAB_RE = __import__("re").compile(r'^===\s+"[^"]*"\s*$')
50+
4251
# Characters that indicate a line is purely structural (not translatable):
4352
# - Markdown headings and list markers
4453
# - Table separators
@@ -57,6 +66,11 @@ def _is_translatable_paragraph(paragraph: str) -> bool:
5766
if COMMENT_RE.match(text):
5867
return False
5968

69+
# Skip MkDocs Material structural directives that have no
70+
# translatable text content (e.g. bare "!!! note").
71+
if ADMONITION_RE.match(text) and '"' not in text:
72+
return False
73+
6074
# Count characters that typically appear in natural language.
6175
alpha = sum(1 for c in text if c.isalpha())
6276
if alpha == 0:
@@ -191,9 +205,26 @@ def _relative_to_source(path: Path) -> str:
191205
return str(path.relative_to(SOURCE_DIR))
192206

193207

194-
def process_file(source_path: Path, dry_run: bool = False) -> bool:
208+
def _write_po_from_paragraphs(po_path: Path, rel: str, paragraphs: list[str], dry_run: bool = False) -> bool:
209+
"""Write a fresh .po file from extracted paragraphs, discarding any existing translations."""
210+
header = _po_header(str(rel))
211+
body_entries = "\n\n".join(f'msgid "{p.replace(chr(34), chr(92) + chr(34))}"\nmsgstr ""' for p in paragraphs)
212+
if dry_run:
213+
print(f" [DRY-RUN] Would force-regenerate: {po_path} ({len(paragraphs)} entries)")
214+
return True
215+
po_path.parent.mkdir(parents=True, exist_ok=True)
216+
po_path.write_text(header + body_entries + "\n", encoding="utf-8")
217+
print(f" Force-regenerated: {po_path} ({len(paragraphs)} entries)")
218+
return True
219+
220+
221+
def process_file(source_path: Path, dry_run: bool = False, force: bool = False) -> bool:
195222
"""Create or update the .po file for *source_path*.
196223
224+
If *force* is True, the entire .po file is regenerated from the source
225+
markdown, discarding any existing translations. Otherwise only new
226+
paragraphs are appended and existing translations are preserved.
227+
197228
Returns True if the .po file was created or modified and contains
198229
at least one untranslated entry.
199230
"""
@@ -209,41 +240,101 @@ def process_file(source_path: Path, dry_run: bool = False) -> bool:
209240
if not paragraphs:
210241
return False
211242

243+
if force and po_path.exists():
244+
return _write_po_from_paragraphs(po_path, str(rel), paragraphs, dry_run)
245+
212246
if not po_path.exists():
213-
# Create new .po file.
214-
header = _po_header(str(rel))
215-
body_entries = "\n\n".join(f'msgid "{p.replace(chr(34), chr(92) + chr(34))}"\nmsgstr ""' for p in paragraphs)
216-
if dry_run:
217-
print(f" [DRY-RUN] Would create: {po_path}")
218-
return True
219-
po_path.parent.mkdir(parents=True, exist_ok=True)
220-
po_path.write_text(header + body_entries + "\n", encoding="utf-8")
221-
print(f" Created: {po_path} ({len(paragraphs)} entries)")
222-
return True
247+
return _write_po_from_paragraphs(po_path, str(rel), paragraphs, dry_run)
223248

224-
# Update existing .po file.
249+
# Incremental update: detect new, removed, and modified paragraphs.
225250
po = pofile(str(po_path))
226-
existing_msgids = {entry.msgid for entry in po if entry.msgid}
251+
entries_by_msgid: dict[str, POEntry] = {}
252+
for entry in po:
253+
if entry.msgid:
254+
entries_by_msgid[entry.msgid] = entry
227255

228256
new_count = 0
257+
modified_count = 0
258+
removed_count = 0
259+
229260
for para in paragraphs:
230-
if para not in existing_msgids:
261+
if para in entries_by_msgid:
262+
continue
263+
# Check if this is a modification of an existing paragraph
264+
# (similar msgid that got updated in the source).
265+
matched = _find_similar_entry(para, entries_by_msgid)
266+
if matched is not None:
267+
old_entry = entries_by_msgid.pop(matched)
268+
new_entry = _build_po_entry(para)
269+
new_entry.msgstr = "" # force re-translation for modified paragraph
270+
po.append(new_entry)
271+
modified_count += 1
272+
else:
231273
po.append(_build_po_entry(para))
232-
existing_msgids.add(para)
233274
new_count += 1
234275

235-
if new_count == 0:
276+
# Mark paragraphs that no longer exist in source as obsolete.
277+
# Any entry still in entries_by_msgid is not in the new paragraphs list.
278+
for old_entry in entries_by_msgid.values():
279+
old_entry.obsolete = True
280+
removed_count += 1
281+
282+
change_count = new_count + modified_count + removed_count
283+
if change_count == 0:
236284
return _has_empty_msgstr(po)
237285

238286
if dry_run:
239-
print(f" [DRY-RUN] Would add {new_count} entries to: {po_path}")
287+
parts = []
288+
if new_count:
289+
parts.append(f"+{new_count} new")
290+
if modified_count:
291+
parts.append(f"~{modified_count} modified")
292+
if removed_count:
293+
parts.append(f"-{removed_count} removed")
294+
print(f" [DRY-RUN] Would update: {po_path} ({', '.join(parts)})")
240295
return True
241296

242297
po.save(str(po_path))
243-
print(f" Updated: {po_path} (+{new_count} new entries)")
298+
parts = []
299+
if new_count:
300+
parts.append(f"+{new_count} new")
301+
if modified_count:
302+
parts.append(f"~{modified_count} modified")
303+
if removed_count:
304+
parts.append(f"-{removed_count} removed")
305+
print(f" Updated: {po_path} ({', '.join(parts)})")
244306
return True
245307

246308

309+
def _find_similar_entry(new_para: str, entries: dict[str, POEntry]) -> str | None:
310+
"""Check if *new_para* is likely a modified version of an existing entry.
311+
312+
Returns the msgid of the matching entry, or None.
313+
Uses a simple heuristic: the first non-trivial line of each paragraph
314+
must be identical, which handles cases where a paragraph was edited
315+
by adding/removing lines in the middle or end.
316+
"""
317+
new_first = _first_significant_line(new_para)
318+
if not new_first:
319+
return None
320+
for msgid in entries:
321+
if _first_significant_line(msgid) == new_first:
322+
return msgid
323+
return None
324+
325+
326+
def _first_significant_line(text: str) -> str:
327+
"""Return the first non-empty, non-link-reference line of *text*."""
328+
for line in text.split("\n"):
329+
stripped = line.strip()
330+
if not stripped:
331+
continue
332+
if stripped.startswith("[") and stripped.endswith(")") and "](" in stripped:
333+
continue
334+
return stripped
335+
return ""
336+
337+
247338
def _has_empty_msgstr(po: POFile) -> bool:
248339
"""Return True if *po* contains at least one entry with an empty msgstr."""
249340
return any(entry.msgid and not entry.msgstr for entry in po if not entry.obsolete)
@@ -261,6 +352,11 @@ def main() -> int:
261352
action="store_true",
262353
help="Do not write .po files, just report changes.",
263354
)
355+
parser.add_argument(
356+
"--force",
357+
action="store_true",
358+
help="Force full regeneration of ALL .po files, discarding existing translations.",
359+
)
264360
args = parser.parse_args()
265361

266362
# Collect English markdown files.
@@ -276,10 +372,13 @@ def main() -> int:
276372

277373
print(f"Scanning {len(en_files)} English markdown files...")
278374

375+
if args.force:
376+
print("--force enabled: regenerating ALL .po files from scratch")
377+
279378
needs_translation: list[str] = []
280379
for source_path in en_files:
281380
try:
282-
if process_file(source_path, dry_run=args.dry_run):
381+
if process_file(source_path, dry_run=args.dry_run, force=args.force):
283382
rel = _relative_to_source(source_path)
284383
po_rel = str(LOCALE_DIR / Path(rel).with_suffix(".po"))
285384
needs_translation.append(po_rel)

0 commit comments

Comments
 (0)