Skip to content

Commit 777942f

Browse files
authored
Merge pull request #119 from Miyamura80/codex/adapt-jules-translation-workflow-for-new-repo-5k1xpv
Fix AI writing check by replacing em dashes with hyphens
2 parents fe6c561 + 7aed5fc commit 777942f

4 files changed

Lines changed: 469 additions & 0 deletions

File tree

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# ─────────────────────────────────────────────────────────────────────
2+
# Jules Translation Sync
3+
# ─────────────────────────────────────────────────────────────────────
4+
# When English source docs change on main, create a Jules session that
5+
# translates all affected pages into every supported locale and opens
6+
# one mega-PR with the results.
7+
#
8+
# To add or remove a locale, edit SUPPORTED_LANGS below (one-line change).
9+
#
10+
# Required secrets: JULES_API_KEY
11+
# ─────────────────────────────────────────────────────────────────────
12+
name: Jules Translation Sync
13+
14+
on:
15+
push:
16+
branches: [main]
17+
paths:
18+
- "docs/content/**"
19+
workflow_dispatch:
20+
21+
permissions:
22+
contents: read
23+
actions: read
24+
25+
concurrency:
26+
group: jules-translation-sync
27+
cancel-in-progress: false
28+
29+
env:
30+
# ── One-line locale config ────────────────────────────────────────
31+
# Comma-separated ISO 639-1 codes matching the .<lang>.mdx convention.
32+
SUPPORTED_LANGS: "es,ja,zh"
33+
# ──────────────────────────────────────────────────────────────────
34+
JULES_API_BASE: "https://jules.googleapis.com/v1alpha"
35+
CHANGED_DOCS_FILE: "changed_english_docs.txt"
36+
MAX_POLL_ATTEMPTS: 60
37+
POLL_INTERVAL_SECONDS: 30
38+
39+
jobs:
40+
sync-translations:
41+
name: Sync translated docs via Jules
42+
runs-on: ubuntu-latest
43+
timeout-minutes: 45
44+
45+
steps:
46+
# ── 1. Checkout ───────────────────────────────────────────────
47+
- name: Checkout repository
48+
uses: actions/checkout@v4
49+
with:
50+
fetch-depth: 0
51+
52+
# ── 2. Find last successful sync commit ─────────────────────
53+
- name: Find last successful sync commit
54+
id: last-success
55+
env:
56+
GH_TOKEN: ${{ github.token }}
57+
run: |
58+
set -euo pipefail
59+
60+
# Find the most recent successful run of this workflow (excluding current)
61+
LAST_SHA=$(gh api \
62+
"repos/${{ github.repository }}/actions/workflows/jules-sync-translations.yml/runs?status=success&branch=main&per_page=1&exclude_pull_requests=true" \
63+
--jq '.workflow_runs[0].head_sha // empty' 2>/dev/null || true)
64+
65+
if [[ -n "$LAST_SHA" ]] && git cat-file -e "${LAST_SHA}^{commit}" 2>/dev/null; then
66+
echo "last_sha=${LAST_SHA}" >> "$GITHUB_OUTPUT"
67+
echo "Found last successful sync at ${LAST_SHA}"
68+
else
69+
echo "last_sha=" >> "$GITHUB_OUTPUT"
70+
echo "No previous successful run found - will diff against parent"
71+
fi
72+
73+
# ── 3. Detect changed English source docs ─────────────────────
74+
- name: Detect changed English source docs
75+
id: detect
76+
env:
77+
LAST_SUCCESS_SHA: ${{ steps.last-success.outputs.last_sha }}
78+
run: |
79+
set -euo pipefail
80+
81+
BEFORE="$LAST_SUCCESS_SHA"
82+
SHA="${{ github.sha }}"
83+
84+
echo "::group::Diff range"
85+
echo "before=${BEFORE}"
86+
echo "sha=${SHA}"
87+
88+
if [[ -z "$BEFORE" ]] || ! git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
89+
echo "No prior successful run - diffing against parent commit"
90+
if git rev-parse --verify "${SHA}^" >/dev/null 2>&1; then
91+
CHANGED=$(git diff --name-only "${SHA}^" "$SHA" -- docs/content/ || true)
92+
else
93+
CHANGED=$(git ls-tree -r --name-only "$SHA" -- docs/content/ || true)
94+
fi
95+
else
96+
CHANGED=$(git diff --name-only "$BEFORE" "$SHA" -- docs/content/ || true)
97+
fi
98+
99+
echo "::endgroup::"
100+
101+
IFS=',' read -ra LANGS <<< "$SUPPORTED_LANGS"
102+
ENGLISH_FILES=()
103+
104+
while IFS= read -r file; do
105+
[[ -z "$file" ]] && continue
106+
[[ "$file" != *.mdx ]] && [[ "$file" != */meta.json ]] && continue
107+
108+
is_translation=false
109+
for lang in "${LANGS[@]}"; do
110+
lang="${lang//[[:space:]]/}"
111+
if [[ -z "$lang" ]]; then
112+
continue
113+
fi
114+
115+
if [[ "$file" == *".${lang}.mdx" ]] || [[ "$file" == */meta.${lang}.json ]]; then
116+
is_translation=true
117+
break
118+
fi
119+
done
120+
121+
if [[ "$is_translation" == "false" ]]; then
122+
ENGLISH_FILES+=("$file")
123+
fi
124+
done <<< "$CHANGED"
125+
126+
if [[ ${#ENGLISH_FILES[@]} -eq 0 ]]; then
127+
echo "No English source doc changes found - nothing to translate."
128+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
129+
exit 0
130+
fi
131+
132+
printf '%s\n' "${ENGLISH_FILES[@]}" > "${RUNNER_TEMP}/${CHANGED_DOCS_FILE}"
133+
134+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
135+
echo "file_count=${#ENGLISH_FILES[@]}" >> "$GITHUB_OUTPUT"
136+
137+
echo "::group::Changed English docs (${#ENGLISH_FILES[@]} files)"
138+
cat "${RUNNER_TEMP}/${CHANGED_DOCS_FILE}"
139+
echo "::endgroup::"
140+
141+
# ── 4. Create Jules translation session ────────────────────────
142+
- name: Create Jules translation session
143+
if: steps.detect.outputs.has_changes == 'true'
144+
id: jules-create
145+
env:
146+
JULES_API_KEY: ${{ secrets.JULES_API_KEY }}
147+
run: |
148+
set -euo pipefail
149+
150+
if [[ -z "${JULES_API_KEY:-}" ]]; then
151+
echo "::error::JULES_API_KEY is not configured. Add it in repository Actions secrets."
152+
exit 1
153+
fi
154+
155+
REPO="${{ github.repository }}"
156+
OWNER="${REPO%%/*}"
157+
REPO_NAME="${REPO##*/}"
158+
SOURCE="sources/github/${OWNER}/${REPO_NAME}"
159+
160+
LANGS_DISPLAY=$(echo "$SUPPORTED_LANGS" | tr ',' ', ')
161+
FILE_LIST=$(cat "${RUNNER_TEMP}/${CHANGED_DOCS_FILE}")
162+
163+
PROMPT="IMPORTANT: Before you begin, read the file docs/translation-guide.md in this repository.
164+
It contains the glossary of never-translate terms, file naming conventions, and detailed
165+
translation rules you must follow strictly.
166+
167+
You are a professional technical translator for this documentation site.
168+
169+
The following English documentation files have been updated:
170+
${FILE_LIST}
171+
172+
For each file listed above, update or create the corresponding translation files for ALL of these locales: ${LANGS_DISPLAY}.
173+
174+
Translation file naming convention:
175+
- English source: path/to/file.mdx
176+
- Translation: path/to/file.<lang>.mdx (e.g. path/to/file.ja.mdx)
177+
- For meta.json: meta.<lang>.json (e.g. docs/content/admin-guide/meta.ja.json)
178+
179+
Strict rules - you MUST follow every one:
180+
1. Preserve ALL Markdown / MDX structure exactly: headings, links, tables, code blocks, admonitions, frontmatter (YAML between ---), and JSX/MDX components.
181+
2. Preserve every anchor ID, link target, and href value verbatim.
182+
3. NEVER translate any of the following - keep them exactly as-is:
183+
- Code snippets and fenced code blocks
184+
- CLI commands and flags (e.g. --verbose, -p, npm install)
185+
- Variable names, function names, class names, configuration keys
186+
- URLs, file paths, email addresses
187+
- Product names and proper nouns
188+
- Technical identifiers, API endpoints, HTTP methods
189+
4. If a translation file already exists, update it so its structure and meaning match the current English source.
190+
5. If a translation file does NOT exist yet, create it as a complete translation of the English source.
191+
6. If content was removed from the English file, remove the corresponding section in every translation file.
192+
7. Only modify files matching docs/content/**/*.<lang>.mdx or docs/content/**/meta.<lang>.json for the supported locales. Do NOT touch any English (.mdx without a locale) files or the base meta.json files.
193+
8. Do NOT modify any files outside docs/content/.
194+
9. In the pull-request description include:
195+
- A list of the English pages that changed
196+
- Which locales were updated
197+
- A note that this is an automated translation and should be reviewed by native speakers
198+
10. For meta.json files in each section directory, create or update meta.<lang>.json. Translate the title field but keep the pages array values unchanged (they are file identifiers, not user-facing text)."
199+
200+
TITLE="docs(i18n): sync translations for updated English docs"
201+
202+
BODY=$(jq -n \
203+
--arg prompt "$PROMPT" \
204+
--arg title "$TITLE" \
205+
--arg source "$SOURCE" \
206+
'{
207+
prompt: $prompt,
208+
title: $title,
209+
automationMode: "AUTO_CREATE_PR",
210+
sourceContext: {
211+
source: $source,
212+
githubRepoContext: {
213+
startingBranch: "main"
214+
}
215+
}
216+
}')
217+
218+
MAX_RETRIES=3
219+
RETRY_DELAY=5
220+
221+
for attempt in $(seq 1 "$MAX_RETRIES"); do
222+
HTTP_CODE=$(curl -s -o /tmp/jules_response.json -w "%{http_code}" \
223+
-X POST \
224+
-H "Content-Type: application/json" \
225+
-H "X-Goog-Api-Key: ${JULES_API_KEY}" \
226+
-d "$BODY" \
227+
"${JULES_API_BASE}/sessions")
228+
229+
if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then
230+
break
231+
elif [[ "$HTTP_CODE" -ge 500 && $attempt -lt $MAX_RETRIES ]]; then
232+
echo "::warning::Jules API returned ${HTTP_CODE} - retrying in ${RETRY_DELAY}s (attempt ${attempt}/${MAX_RETRIES})"
233+
sleep "$RETRY_DELAY"
234+
RETRY_DELAY=$((RETRY_DELAY * 2))
235+
else
236+
echo "::error::Jules session creation failed with HTTP ${HTTP_CODE}"
237+
cat /tmp/jules_response.json
238+
exit 1
239+
fi
240+
done
241+
242+
SESSION_NAME=$(jq -r '.name' /tmp/jules_response.json)
243+
SESSION_URL=$(jq -r '.url // empty' /tmp/jules_response.json)
244+
245+
if [[ -z "$SESSION_NAME" || "$SESSION_NAME" == "null" ]]; then
246+
echo "::error::No session name in Jules response"
247+
cat /tmp/jules_response.json
248+
exit 1
249+
fi
250+
251+
{
252+
echo "session_name<<GHEOF"
253+
echo "$SESSION_NAME"
254+
echo "GHEOF"
255+
echo "session_url<<GHEOF"
256+
echo "$SESSION_URL"
257+
echo "GHEOF"
258+
} >> "$GITHUB_OUTPUT"
259+
260+
echo "Jules session created: ${SESSION_NAME}"
261+
echo "Jules UI: ${SESSION_URL}"
262+
263+
# ── 5. Poll until Jules finishes ───────────────────────────────
264+
- name: Poll Jules session until completion
265+
if: steps.detect.outputs.has_changes == 'true'
266+
id: jules-poll
267+
env:
268+
JULES_API_KEY: ${{ secrets.JULES_API_KEY }}
269+
JULES_SESSION_NAME: ${{ steps.jules-create.outputs.session_name }}
270+
run: |
271+
set -euo pipefail
272+
273+
SESSION_NAME="$JULES_SESSION_NAME"
274+
MAX_ATTEMPTS="$MAX_POLL_ATTEMPTS"
275+
INTERVAL="$POLL_INTERVAL_SECONDS"
276+
277+
echo "Polling ${SESSION_NAME} (max ${MAX_ATTEMPTS} x ${INTERVAL}s)"
278+
279+
for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
280+
RETRY_DELAY=5
281+
for retry in 1 2 3; do
282+
HTTP_CODE=$(curl -s -o /tmp/jules_session.json -w "%{http_code}" \
283+
-H "X-Goog-Api-Key: ${JULES_API_KEY}" \
284+
"${JULES_API_BASE}/${SESSION_NAME}")
285+
286+
if [[ "$HTTP_CODE" -ge 200 && "$HTTP_CODE" -lt 300 ]]; then
287+
break
288+
elif [[ $retry -lt 3 ]]; then
289+
echo "::warning::Poll returned ${HTTP_CODE} - retry ${retry}/3 in ${RETRY_DELAY}s"
290+
sleep "$RETRY_DELAY"
291+
RETRY_DELAY=$((RETRY_DELAY * 2))
292+
else
293+
echo "::error::Failed to poll session after 3 retries (HTTP ${HTTP_CODE})"
294+
exit 1
295+
fi
296+
done
297+
298+
STATE=$(jq -r '.state // "UNKNOWN"' /tmp/jules_session.json)
299+
echo "[${attempt}/${MAX_ATTEMPTS}] state=${STATE}"
300+
301+
case "$STATE" in
302+
COMPLETED)
303+
PR_URL=$(jq -r '.outputs[0].pullRequest.url // empty' /tmp/jules_session.json)
304+
PR_TITLE=$(jq -r '.outputs[0].pullRequest.title // empty' /tmp/jules_session.json)
305+
306+
{
307+
echo "jules_state<<GHEOF"
308+
echo "COMPLETED"
309+
echo "GHEOF"
310+
echo "pr_url<<GHEOF"
311+
echo "$PR_URL"
312+
echo "GHEOF"
313+
echo "pr_title<<GHEOF"
314+
echo "$PR_TITLE"
315+
echo "GHEOF"
316+
} >> "$GITHUB_OUTPUT"
317+
318+
echo "Session completed. PR: ${PR_URL}"
319+
exit 0
320+
;;
321+
FAILED)
322+
echo "jules_state=FAILED" >> "$GITHUB_OUTPUT"
323+
echo "::error::Jules session ended in FAILED state"
324+
jq '.' /tmp/jules_session.json
325+
exit 1
326+
;;
327+
esac
328+
329+
sleep "$INTERVAL"
330+
done
331+
332+
echo "jules_state=TIMEOUT" >> "$GITHUB_OUTPUT"
333+
echo "::error::Timed out after ${MAX_ATTEMPTS} poll attempts"
334+
exit 1

CLAUDE.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,13 @@ Structure as: `init()` → `continue(id)` → `cleanup(id)`
142142
## Deprecated
143143

144144
- Don't use `datetime.utcnow()` - use `datetime.now(timezone.utc)`
145+
146+
---
147+
148+
## Automated Translation (Jules Sync)
149+
150+
Docs under `docs/content/` are auto-translated by the **Jules Translation Sync**
151+
workflow. Do NOT manually translate doc files - edit the English source and the
152+
workflow will update all locales (`es`, `ja`, `zh`).
153+
See [`docs/translation-guide.md`](docs/translation-guide.md) for the full
154+
glossary, file naming conventions, and translation rules.

0 commit comments

Comments
 (0)