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
63 changes: 62 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@
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"
runs:
using: "composite"
steps:
Expand Down Expand Up @@ -124,14 +128,71 @@
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"

Check failure on line 147 in action.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

inputs.repo_ig is vulnerable to script injection: values of inputs are provided by whoever triggers the workflow. Change this action to not use user-controlled data directly in a run block, for example by assigning this expression to an environment variable.

See more on https://sonarcloud.io/project/issues?id=ansforge_IG-workflows&issues=AZ5y1qTfehU7qPRFjLNv&open=AZ5y1qTfehU7qPRFjLNv&pullRequest=34
DEST="${{ inputs.repo_ig }}/input/translations/en"

Check failure on line 148 in action.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

inputs.repo_ig is vulnerable to script injection: values of inputs are provided by whoever triggers the workflow. Change this action to not use user-controlled data directly in a run block, for example by assigning this expression to an environment variable.

See more on https://sonarcloud.io/project/issues?id=ansforge_IG-workflows&issues=AZ5y1qTfehU7qPRFjLNw&open=AZ5y1qTfehU7qPRFjLNw&pullRequest=34
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: ${{ env.ALBERT_WORKFLOW_KEY }}
run: |
FORCE_FLAG=""
if [ "${{ inputs.translate }}" = "force" ]; then

Check failure on line 165 in action.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

inputs.translate is vulnerable to script injection: values of inputs are provided by whoever triggers the workflow. Change this action to not use user-controlled data directly in a run block, for example by assigning this expression to an environment variable.

See more on https://sonarcloud.io/project/issues?id=ansforge_IG-workflows&issues=AZ5y1qTfehU7qPRFjLNx&open=AZ5y1qTfehU7qPRFjLNx&pullRequest=34
FORCE_FLAG="--force"
fi
python ${{ github.action_path }}/tools/translate_pages.py \
--source-dir "${{ inputs.repo_ig }}/input/pagecontent" \

Check failure on line 169 in action.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

inputs.repo_ig is vulnerable to script injection: values of inputs are provided by whoever triggers the workflow. Change this action to not use user-controlled data directly in a run block, for example by assigning this expression to an environment variable.

See more on https://sonarcloud.io/project/issues?id=ansforge_IG-workflows&issues=AZ5y1qTfehU7qPRFjLNy&open=AZ5y1qTfehU7qPRFjLNy&pullRequest=34
--target-dir "${{ inputs.repo_ig }}/input/translations/en/pagecontent" \

Check failure on line 170 in action.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

inputs.repo_ig is vulnerable to script injection: values of inputs are provided by whoever triggers the workflow. Change this action to not use user-controlled data directly in a run block, for example by assigning this expression to an environment variable.

See more on https://sonarcloud.io/project/issues?id=ansforge_IG-workflows&issues=AZ5y1qTfehU7qPRFjLNz&open=AZ5y1qTfehU7qPRFjLNz&pullRequest=34
$FORCE_FLAG

- name: 🌐 Commit translated pages
if: ${{ inputs.translate == 'true' || inputs.translate == 'force' }}
shell: bash
run: |
cd ${{ inputs.repo_ig }}

Check failure on line 177 in action.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

inputs.repo_ig is vulnerable to script injection: values of inputs are provided by whoever triggers the workflow. Change this action to not use user-controlled data directly in a run block, for example by assigning this expression to an environment variable.

See more on https://sonarcloud.io/project/issues?id=ansforge_IG-workflows&issues=AZ5y1qTfehU7qPRFjLN0&open=AZ5y1qTfehU7qPRFjLN0&pullRequest=34
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
run: |
if [ ${{inputs.ig-publisher-version}} = "latest" ]; then

Check failure on line 192 in action.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

inputs.ig-publisher-version is vulnerable to script injection: values of inputs are provided by whoever triggers the workflow. Change this action to not use user-controlled data directly in a run block, for example by assigning this expression to an environment variable.

See more on https://sonarcloud.io/project/issues?id=ansforge_IG-workflows&issues=AZ5y1qTfehU7qPRFjLN1&open=AZ5y1qTfehU7qPRFjLN1&pullRequest=34
wget -q https://github.com/HL7/fhir-ig-publisher/releases/latest/download/publisher.jar
else
wget -q https://github.com/HL7/fhir-ig-publisher/releases/download/${{inputs.ig-publisher-version}}/publisher.jar

Check failure on line 195 in action.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

inputs.ig-publisher-version is vulnerable to script injection: values of inputs are provided by whoever triggers the workflow. Change this action to not use user-controlled data directly in a run block, for example by assigning this expression to an environment variable.

See more on https://sonarcloud.io/project/issues?id=ansforge_IG-workflows&issues=AZ5y1qTfehU7qPRFjLN2&open=AZ5y1qTfehU7qPRFjLN2&pullRequest=34
fi

#Récupération du nom de la branche afin de publier dans gh-pages
Expand Down
131 changes: 131 additions & 0 deletions tools/translate_pages.py
Original file line number Diff line number Diff line change
@@ -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()