From f61e4649d689469093af9cd8c9a94e1140fb2f0d Mon Sep 17 00:00:00 2001 From: Nicolas Riss <48218773+nriss@users.noreply.github.com> Date: Fri, 29 May 2026 10:24:40 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20ajout=20de=20la=20traduction=20auto?= =?UTF-8?q?matique=20FR=E2=86=92EN=20des=20pages=20pagecontent=20via=20Alb?= =?UTF-8?q?ert=20IA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deux nouveaux inputs dans action.yml : - translate : false (défaut) / true (nouvelles pages) / force (tout retraduire) - albert_api_key : clé API Albert à passer depuis le workflow appelant (ALBERT_WORKFLOW_KEY) Les steps de traduction s'exécutent entre sushi et le publisher, garantissant que les pages traduites sont disponibles lors de la génération HTML. Ajout du script tools/translate_pages.py. Co-Authored-By: Claude Sonnet 4.6 --- action.yml | 67 +++++++++++++++++++- tools/translate_pages.py | 131 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 tools/translate_pages.py diff --git a/action.yml b/action.yml index aa2c0ee..7fde251 100644 --- a/action.yml +++ b/action.yml @@ -46,7 +46,15 @@ inputs: publish_path_outpout: description: "Chemin de publication de l'IG." required: false - default: "" + default: "" + translate: + description: "Traduire les pages pagecontent FR→EN avant le build via Albert IA (false / true / force)." + required: false + default: "false" + albert_api_key: + description: "Clé API Albert (ALBERT_WORKFLOW_KEY) pour la traduction automatique des pages." + required: false + default: "" runs: using: "composite" steps: @@ -124,6 +132,63 @@ runs: shell: bash run: sushi ${{ inputs.repo_ig}} + # Traduction automatique FR→EN des pages pagecontent via Albert IA + - name: 🌐 Setup Python for translation + if: ${{ inputs.translate == 'true' || inputs.translate == 'force' }} + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: 🌐 Install httpx + if: ${{ inputs.translate == 'true' || inputs.translate == 'force' }} + shell: bash + run: pip install httpx + + - name: 🌐 Sync PO files (translations/en/po → input/translations/en) + if: ${{ inputs.translate == 'true' || inputs.translate == 'force' }} + shell: bash + run: | + SRC="${{ inputs.repo_ig }}/translations/en/po" + DEST="${{ inputs.repo_ig }}/input/translations/en" + if [ -d "$SRC" ] && ls "$SRC"/*.po 2>/dev/null | grep -q .; then + echo "Syncing .po files from $SRC to $DEST..." + mkdir -p "$DEST" + cp "$SRC"/*.po "$DEST"/ + echo "Synced $(ls "$SRC"/*.po | wc -l) file(s)." + else + echo "No .po files found in $SRC — skipping." + fi + + - name: 🌐 Translate pagecontent (FR→EN) + if: ${{ inputs.translate == 'true' || inputs.translate == 'force' }} + shell: bash + env: + ALBERT_API_KEY: ${{ inputs.albert_api_key }} + run: | + FORCE_FLAG="" + if [ "${{ inputs.translate }}" = "force" ]; then + FORCE_FLAG="--force" + fi + python ${{ github.action_path }}/tools/translate_pages.py \ + --source-dir "${{ inputs.repo_ig }}/input/pagecontent" \ + --target-dir "${{ inputs.repo_ig }}/input/translations/en/pagecontent" \ + $FORCE_FLAG + + - name: 🌐 Commit translated pages + if: ${{ inputs.translate == 'true' || inputs.translate == 'force' }} + shell: bash + run: | + cd ${{ inputs.repo_ig }} + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add input/translations/en/ + if git diff --cached --quiet; then + echo "No translation changes to commit." + else + git commit -m "chore: auto-translate pagecontent FR→EN via Albert IA" + git push + fi + # Téléchargement de la dernière version du publisher - name: 📥 Download IG Publisher shell: bash diff --git a/tools/translate_pages.py b/tools/translate_pages.py new file mode 100644 index 0000000..0e8d77b --- /dev/null +++ b/tools/translate_pages.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +"""Translate IG pagecontent Markdown files from French to English using Albert API.""" + +import os +import sys +import argparse +import httpx +from pathlib import Path + +ALBERT_API_URL = "https://albert.api.etalab.gouv.fr/v1/chat/completions" +DEFAULT_MODEL = "mistralai/Mistral-Small-3.2-24B-Instruct-2506" + +SYSTEM_PROMPT = """You are a medical/technical translator specializing in French healthcare interoperability documentation (FHIR Implementation Guides). + +Translate the following Markdown/HTML content from French to English. + +Strict rules: +- Preserve ALL technical elements exactly as-is without any modification: + * Liquid/Jekyll tags: {% sql ... %}, {% include ... %}, {% lang-fragment ... %} + * HTML tags and their attributes (style, class, src, alt, title, etc.) + * FHIR resource names, profile identifiers, search parameter names + * French code identifiers: TRE_xxx, JDV_xxx, ASS_xxx, flux names (e.g. Flux1, CreationCercleSoins) + * URLs, canonical links, anchor links + * Image src paths (e.g. sf_image1.png) + * Markdown structure: heading levels (###, ####, etc.), tables, lists, bold/italic + * Code blocks (``` ... ```) — do not translate their content +- Translate ONLY the human-readable French prose and labels +- Do NOT add any explanations, preamble, or trailing comment +- Return ONLY the translated content""" + + +def translate_content(content: str, token: str, model: str) -> str: + response = httpx.post( + ALBERT_API_URL, + headers={"Authorization": f"Bearer {token}"}, + json={ + "model": model, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": content}, + ], + "max_tokens": 8192, + "temperature": 0.1, + }, + timeout=120.0, + ) + response.raise_for_status() + return response.json()["choices"][0]["message"]["content"] + + +def main(): + parser = argparse.ArgumentParser( + description="Translate IG pagecontent from French to English with Albert API" + ) + parser.add_argument( + "--source-dir", + default="input/pagecontent", + help="Directory containing the French .md source files (default: input/pagecontent)", + ) + parser.add_argument( + "--target-dir", + default="input/translations/en/pagecontent", + help="Directory where English translations will be written (default: input/translations/en/pagecontent)", + ) + parser.add_argument( + "--model", + default=DEFAULT_MODEL, + help=f"Albert model to use (default: {DEFAULT_MODEL})", + ) + parser.add_argument( + "--force", + action="store_true", + help="Retranslate files even if a translation already exists", + ) + args = parser.parse_args() + + token = os.environ.get("ALBERT_API_KEY") + if not token: + print("Error: ALBERT_API_KEY environment variable is not set", file=sys.stderr) + sys.exit(1) + + source_dir = Path(args.source_dir) + target_dir = Path(args.target_dir) + + if not source_dir.is_dir(): + print(f"Error: source directory not found: {source_dir}", file=sys.stderr) + sys.exit(1) + + target_dir.mkdir(parents=True, exist_ok=True) + + md_files = sorted(source_dir.glob("*.md")) + if not md_files: + print(f"No .md files found in {source_dir}") + return + + translated_count = 0 + skipped_count = 0 + + for source_file in md_files: + target_file = target_dir / source_file.name + content = source_file.read_text(encoding="utf-8") + + if not content.strip(): + print(f"Skipping {source_file.name} (empty file)") + skipped_count += 1 + continue + + if target_file.exists() and not args.force: + print(f"Skipping {source_file.name} (translation exists — use --force to retranslate)") + skipped_count += 1 + continue + + print(f"Translating {source_file.name}...", end=" ", flush=True) + try: + translated = translate_content(content, token, args.model) + target_file.write_text(translated, encoding="utf-8") + print("OK") + translated_count += 1 + except httpx.HTTPStatusError as e: + print(f"FAILED (HTTP {e.response.status_code})", file=sys.stderr) + print(e.response.text, file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"FAILED: {e}", file=sys.stderr) + sys.exit(1) + + print(f"\nDone: {translated_count} translated, {skipped_count} skipped.") + + +if __name__ == "__main__": + main() From 2088b91dc57a421d04a3767db231db72da0e5d87 Mon Sep 17 00:00:00 2001 From: Nicolas Riss <48218773+nriss@users.noreply.github.com> Date: Fri, 29 May 2026 10:34:52 +0200 Subject: [PATCH 2/2] =?UTF-8?q?refactor:=20suppression=20de=20l'input=20al?= =?UTF-8?q?bert=5Fapi=5Fkey=20=E2=80=94=20lecture=20via=20env=20ALBERT=5FW?= =?UTF-8?q?ORKFLOW=5FKEY?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La clé API Albert n'est plus un paramètre explicite de l'action. L'action lit directement la variable d'environnement ALBERT_WORKFLOW_KEY, gérée au niveau de l'organisation ansforge. Les IGs appelants n'ont pas à connaître ni à configurer cette clé. Co-Authored-By: Claude Sonnet 4.6 --- action.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/action.yml b/action.yml index 7fde251..2303284 100644 --- a/action.yml +++ b/action.yml @@ -51,10 +51,6 @@ inputs: description: "Traduire les pages pagecontent FR→EN avant le build via Albert IA (false / true / force)." required: false default: "false" - albert_api_key: - description: "Clé API Albert (ALBERT_WORKFLOW_KEY) pour la traduction automatique des pages." - required: false - default: "" runs: using: "composite" steps: @@ -163,7 +159,7 @@ runs: if: ${{ inputs.translate == 'true' || inputs.translate == 'force' }} shell: bash env: - ALBERT_API_KEY: ${{ inputs.albert_api_key }} + ALBERT_API_KEY: ${{ env.ALBERT_WORKFLOW_KEY }} run: | FORCE_FLAG="" if [ "${{ inputs.translate }}" = "force" ]; then