diff --git a/app/analyse/interface.py b/app/analyse/interface.py index b8a68c2..3eb0078 100644 --- a/app/analyse/interface.py +++ b/app/analyse/interface.py @@ -1,4 +1,5 @@ import streamlit as st +from utils.translations import _ from .sankey import afficher_sankey @@ -30,11 +31,11 @@ def preparer_graphe(G): def selectionner_niveaux(): """Interface pour sélectionner les niveaux de départ et d'arrivée.""" - st.markdown("## Sélection des nœuds de départ et d'arrivée") - valeur_defaut = "-- Sélectionner un niveau --" + st.markdown(f"## {str(_('pages.analyse.selection_nodes', 'Sélection des nœuds de départ et d\'arrivée'))}") + valeur_defaut = str(_("pages.analyse.select_level", "-- Sélectionner un niveau --")) niveau_choix = [valeur_defaut] + list(niveau_labels.values()) - niveau_depart = st.selectbox("Niveau de départ", niveau_choix, key="analyse_niveau_depart") + niveau_depart = st.selectbox(str(_("pages.analyse.start_level", "Niveau de départ")), niveau_choix, key="analyse_niveau_depart") if niveau_depart == valeur_defaut: return None, None @@ -42,7 +43,7 @@ def selectionner_niveaux(): niveaux_arrivee_possibles = [v for k, v in niveau_labels.items() if k > niveau_depart_int] niveaux_arrivee_choix = [valeur_defaut] + niveaux_arrivee_possibles - analyse_niveau_arrivee = st.selectbox("Niveau d'arrivée", niveaux_arrivee_choix, key="analyse_niveau_arrivee") + analyse_niveau_arrivee = st.selectbox(str(_("pages.analyse.end_level", "Niveau d'arrivée")), niveaux_arrivee_choix, key="analyse_niveau_arrivee") if analyse_niveau_arrivee == valeur_defaut: return niveau_depart_int, None @@ -54,7 +55,7 @@ def selectionner_minerais(G, niveau_depart, niveau_arrivee): """Interface pour sélectionner les minerais si nécessaire.""" minerais_selection = None if niveau_depart < 2 < niveau_arrivee: - st.markdown("### Sélectionner un ou plusieurs minerais") + st.markdown(f"### {str(_('pages.analyse.select_minerals', 'Sélectionner un ou plusieurs minerais'))}") # Tous les nœuds de niveau 2 (minerai) minerais_nodes = sorted([ n for n, d in G.nodes(data=True) @@ -62,7 +63,7 @@ def selectionner_minerais(G, niveau_depart, niveau_arrivee): ]) minerais_selection = st.multiselect( - "Filtrer par minerais (optionnel)", + str(_("pages.analyse.filter_by_minerals", "Filtrer par minerais (optionnel)")), minerais_nodes, key="analyse_minerais" ) @@ -73,15 +74,15 @@ def selectionner_minerais(G, niveau_depart, niveau_arrivee): def selectionner_noeuds(G, niveaux_temp, niveau_depart, niveau_arrivee): """Interface pour sélectionner les nœuds spécifiques de départ et d'arrivée.""" st.markdown("---") - st.markdown("## Sélection fine des items") + st.markdown(f"## {str(_('pages.analyse.fine_selection', 'Sélection fine des items'))}") depart_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_depart] arrivee_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_arrivee] - noeuds_depart = st.multiselect("Filtrer par noeuds de départ (optionnel)", + noeuds_depart = st.multiselect(str(_("pages.analyse.filter_start_nodes", "Filtrer par noeuds de départ (optionnel)")), sorted(depart_nodes), key="analyse_noeuds_depart") - noeuds_arrivee = st.multiselect("Filtrer par noeuds d'arrivée (optionnel)", + noeuds_arrivee = st.multiselect(str(_("pages.analyse.filter_end_nodes", "Filtrer par noeuds d'arrivée (optionnel)")), sorted(arrivee_nodes), key="analyse_noeuds_arrivee") @@ -94,26 +95,26 @@ def selectionner_noeuds(G, niveaux_temp, niveau_depart, niveau_arrivee): def configurer_filtres_vulnerabilite(): """Interface pour configurer les filtres de vulnérabilité.""" st.markdown("---") - st.markdown("## Sélection des filtres pour identifier les vulnérabilités") + st.markdown(f"## {str(_('pages.analyse.vulnerability_filters', 'Sélection des filtres pour identifier les vulnérabilités'))}") - filtrer_ics = st.checkbox("Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)", + filtrer_ics = st.checkbox(str(_("pages.analyse.filter_ics", "Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)")), key="analyse_filtrer_ics") - filtrer_ivc = st.checkbox("Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)", + filtrer_ivc = st.checkbox(str(_("pages.analyse.filter_ivc", "Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)")), key="analyse_filtrer_ivc") - filtrer_ihh = st.checkbox("Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)", + filtrer_ihh = st.checkbox(str(_("pages.analyse.filter_ihh", "Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)")), key="analyse_filtrer_ihh") ihh_type = "Pays" if filtrer_ihh: - ihh_type = st.radio("Appliquer le filtre IHH sur :", - ["Pays", "Acteurs"], + ihh_type = st.radio(str(_("pages.analyse.apply_ihh_filter", "Appliquer le filtre IHH sur :")), + [str(_("pages.analyse.countries", "Pays")), str(_("pages.analyse.actors", "Acteurs"))], horizontal=True, key="analyse_ihh_type") - filtrer_isg = st.checkbox("Filtrer les chemins contenant un pays instable (ISG ≥ 60)", + filtrer_isg = st.checkbox(str(_("pages.analyse.filter_isg", "Filtrer les chemins contenant un pays instable (ISG ≥ 60)")), key="analyse_filtrer_isg") - logique_filtrage = st.radio("Logique de filtrage", - ["OU", "ET"], + logique_filtrage = st.radio(str(_("pages.analyse.filter_logic", "Logique de filtrage")), + [str(_("pages.analyse.or", "OU")), str(_("pages.analyse.and", "ET"))], horizontal=True, key="analyse_logique_filtrage") @@ -121,16 +122,16 @@ def configurer_filtres_vulnerabilite(): def interface_analyse(G_temp): - st.markdown("# Analyse du graphe") - with st.expander("Comment utiliser cet onglet ?", expanded=False): - st.markdown(""" - 1. Sélectionnez le niveau de départ (produit final, composant ou minerai) - 2. Choisissez le niveau d'arrivée souhaité - 3. Affinez votre sélection en spécifiant soit un ou des minerais à cibler spécifiquement ou des items précis à chaque niveau (optionnel) - 4. Définissez les critères d'analyse en sélectionnant les indices de vulnérabilité pertinents - 5. Choisissez le mode de combinaison des indices (ET/OU) selon votre besoin d'analyse - 6. Explorez le graphique généré en utilisant les contrôles de zoom et de déplacement ; vous pouvez basculer en mode plein écran pour le graphe - """) + st.markdown(f"# {str(_('pages.analyse.title', 'Analyse du graphe'))}") + with st.expander(str(_("pages.analyse.help", "Comment utiliser cet onglet ?")), expanded=False): + st.markdown("\n".join(_("pages.analyse.help_content", [ + "1. Sélectionnez le niveau de départ (produit final, composant ou minerai)", + "2. Choisissez le niveau d'arrivée souhaité", + "3. Affinez votre sélection en spécifiant soit un ou des minerais à cibler spécifiquement ou des items précis à chaque niveau (optionnel)", + "4. Définissez les critères d'analyse en sélectionnant les indices de vulnérabilité pertinents", + "5. Choisissez le mode de combinaison des indices (ET/OU) selon votre besoin d'analyse", + "6. Explorez le graphique généré en utilisant les contrôles de zoom et de déplacement ; vous pouvez basculer en mode plein écran pour le graphe" + ]))) st.markdown("---") try: @@ -154,7 +155,7 @@ def interface_analyse(G_temp): # Lancement de l'analyse st.markdown("---") - if st.button("Lancer l'analyse", type="primary", key="analyse_lancer"): + if st.button(str(_("pages.analyse.run_analysis", "Lancer l'analyse")), type="primary", key="analyse_lancer"): afficher_sankey( G_temp, niveau_depart=niveau_depart, @@ -171,4 +172,4 @@ def interface_analyse(G_temp): ) except Exception as e: - st.error(f"Erreur de prévisualisation du graphe : {e}") + st.error(f"{str(_('errors.graph_preview_error', 'Erreur de prévisualisation du graphe :'))} {e}") diff --git a/app/analyse/sankey.py b/app/analyse/sankey.py index 671747c..6a2dec8 100644 --- a/app/analyse/sankey.py +++ b/app/analyse/sankey.py @@ -5,6 +5,7 @@ import plotly.graph_objects as go import networkx as nx import logging import tempfile +from utils.translations import _ from utils.graph_utils import ( extraire_chemins_depuis, @@ -188,11 +189,11 @@ def edge_info(G, u, v): """Génère l'info-bulle pour un lien""" data = G.get_edge_data(u, v) if not data: - return f"Relation : {u} → {v}" + return f"{str(_('pages.analyse.sankey.relation', 'Relation'))} : {u} → {v}" if isinstance(data, dict) and all(isinstance(k, int) for k in data): data = data[0] base = [f"{k}: {v}" for k, v in data.items()] - return f"Relation : {u} → {v}
" + "
".join(base) + return f"{str(_('pages.analyse.sankey.relation', 'Relation'))} : {u} → {v}
" + "
".join(base) def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins): """Prépare les données pour le graphique Sankey""" @@ -272,7 +273,7 @@ def creer_graphique_sankey(G, niveaux, df_liens, sorted_nodes, customdata, link_ )) fig.update_layout( - title_text="Hiérarchie filtrée par niveaux et noeuds", + title_text=str(_("pages.analyse.sankey.filtered_hierarchy", "Hiérarchie filtrée par niveaux et noeuds")), paper_bgcolor="white", plot_bgcolor="white" ) @@ -302,7 +303,7 @@ def exporter_graphe_filtre(G, liens_chemins): with open(dot_path, encoding="utf-8") as f: st.download_button( - label="Télécharger le fichier DOT filtré", + label=str(_("pages.analyse.sankey.download_dot", "Télécharger le fichier DOT filtré")), data=f.read(), file_name="graphe_filtré.dot", mime="text/plain" @@ -324,7 +325,7 @@ def afficher_sankey( chemins = extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais) if not chemins: - st.warning("Aucun chemin trouvé pour les critères spécifiés.") + st.warning(str(_("pages.analyse.sankey.no_paths", "Aucun chemin trouvé pour les critères spécifiés."))) return # Étape 3 : Filtrage des chemins selon les critères de vulnérabilité @@ -334,7 +335,7 @@ def afficher_sankey( ) if not liens_chemins: - st.warning("Aucun chemin ne correspond aux critères.") + st.warning(str(_("pages.analyse.sankey.no_matching_paths", "Aucun chemin ne correspond aux critères."))) return # Étape 4 : Préparation des données pour le graphique Sankey diff --git a/app/fiches/interface.py b/app/fiches/interface.py index 7e9d8fb..eebc042 100644 --- a/app/fiches/interface.py +++ b/app/fiches/interface.py @@ -3,6 +3,7 @@ import streamlit as st import requests import os import pathlib +from utils.translations import _ from .utils.tickets.display import afficher_tickets_par_fiche from .utils.tickets.creation import formulaire_creation_ticket_dynamique @@ -17,21 +18,23 @@ from .utils.fiche_utils import load_seuils, doit_regenerer_fiche from .generer import generer_fiche def interface_fiches(): - st.markdown("# Découverte des fiches") - with st.expander("Comment utiliser cet onglet ?", expanded=False): - st.markdown(""" - 1. Parcourez la liste des fiches disponibles par catégorie - 2. Sélectionnez une fiche pour afficher son contenu complet - 3. Consultez les données détaillées, graphiques et analyses supplémentaires - 4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées - - Les catégories sont les suivantes : - * Assemblage : opération d'assemblage des produits finaux à partir des composants - * Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition - * Criticités : indices utilisés pour identifier et évaluer les vulnérabilités - * Fabrication : opération de fabrication des composants à partir de minerais - * Minerai : description et opérations d'extraction et de traitement des minerais - """) + st.markdown(f"# {str(_('pages.fiches.title', 'Découverte des fiches'))}") + with st.expander(str(_("pages.fiches.help", "Comment utiliser cet onglet ?")), expanded=False): + st.markdown("\n".join([ + " " + line for line in _("pages.fiches.help_content", [ + "1. Parcourez la liste des fiches disponibles par catégorie", + "2. Sélectionnez une fiche pour afficher son contenu complet", + "3. Consultez les données détaillées, graphiques et analyses supplémentaires", + "4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées", + "", + "Les catégories sont les suivantes :", + "* Assemblage : opération d'assemblage des produits finaux à partir des composants", + "* Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition", + "* Criticités : indices utilisés pour identifier et évaluer les vulnérabilités", + "* Fabrication : opération de fabrication des composants à partir de minerais", + "* Minerai : description et opérations d'extraction et de traitement des minerais" + ]) + ])) st.markdown("---") if "fiches_arbo" not in st.session_state: @@ -39,18 +42,24 @@ def interface_fiches(): arbo = st.session_state.get("fiches_arbo", {}) if not arbo: - st.warning("Aucune fiche disponible pour le moment.") + st.warning(str(_("pages.fiches.no_files", "Aucune fiche disponible pour le moment."))) return dossiers = sorted(arbo.keys(), key=lambda x: x.lower()) - dossier_choisi = st.selectbox("Choisissez une catégorie de fiches", ["-- Sélectionner un dossier --"] + dossiers) + dossier_choisi = st.selectbox( + str(_("pages.fiches.choose_category", "Choisissez une catégorie de fiches")), + [str(_("pages.fiches.select_folder", "-- Sélectionner un dossier --"))] + dossiers + ) - if dossier_choisi and dossier_choisi != "-- Sélectionner un dossier --": + if dossier_choisi and dossier_choisi != str(_("pages.fiches.select_folder", "-- Sélectionner un dossier --")): fiches = arbo.get(dossier_choisi, []) noms_fiches = [f['nom'] for f in fiches] - fiche_choisie = st.selectbox("Choisissez une fiche", ["-- Sélectionner une fiche --"] + noms_fiches) + fiche_choisie = st.selectbox( + str(_("pages.fiches.choose_file", "Choisissez une fiche")), + [str(_("pages.fiches.select_file", "-- Sélectionner une fiche --"))] + noms_fiches + ) - if fiche_choisie and fiche_choisie != "-- Sélectionner une fiche --": + if fiche_choisie and fiche_choisie != str(_("pages.fiches.select_file", "-- Sélectionner une fiche --")): fiche_info = next((f for f in fiches if f["nom"] == fiche_choisie), None) if fiche_info: try: @@ -92,19 +101,19 @@ def interface_fiches(): if os.path.exists(pdf_path): with open(pdf_path, "rb") as pdf_file: st.download_button( - label="Télécharger cette fiche en PDF", + label=str(_("pages.fiches.download_pdf", "Télécharger cette fiche en PDF")), data=pdf_file, file_name=pdf_name, mime="application/pdf", - help="Télécharger la version PDF de cette fiche", + help=str(_("pages.fiches.download_pdf", "Télécharger cette fiche en PDF")), key="telecharger_fiche_pdf" ) else: - st.warning("Le fichier PDF de cette fiche n'est pas disponible.") + st.warning(str(_("pages.fiches.pdf_unavailable", "Le fichier PDF de cette fiche n'est pas disponible."))) - st.markdown("## Gestion des tickets pour cette fiche") + st.markdown(f"## {str(_('pages.fiches.ticket_management', 'Gestion des tickets pour cette fiche'))}") afficher_tickets_par_fiche(rechercher_tickets_gitea(fiche_choisie)) formulaire_creation_ticket_dynamique(fiche_choisie) except Exception as e: - st.error(f"Erreur lors du chargement de la fiche : {e}") + st.error(f"{str(_('pages.fiches.loading_error', 'Erreur lors du chargement de la fiche :'))} {e}") diff --git a/app/fiches/utils/tickets/core.py b/app/fiches/utils/tickets/core.py index 99a88e5..26f2b69 100644 --- a/app/fiches/utils/tickets/core.py +++ b/app/fiches/utils/tickets/core.py @@ -5,6 +5,7 @@ import json import requests import os import streamlit as st +from utils.translations import _ from config import GITEA_URL, GITEA_TOKEN, ORGANISATION, DEPOT_FICHES, ENV @@ -16,7 +17,7 @@ def gitea_request(method, url, **kwargs): response.raise_for_status() return response except requests.RequestException as e: - st.error(f"Erreur Gitea ({method.upper()}): {e}") + st.error(f"{str(_('errors.gitea_error', 'Erreur Gitea'))} ({method.upper()}): {e}") return None @@ -38,9 +39,9 @@ def charger_fiches_et_labels(): "item": item.strip() } except FileNotFoundError: - st.error(f"❌ Le fichier {chemin_csv} est introuvable.") + st.error(f"❌ {str(_('errors.file_not_found', 'Le fichier'))} {chemin_csv} {str(_('errors.is_missing', 'est introuvable.'))}") except Exception as e: - st.error(f"❌ Erreur lors du chargement des fiches : {str(e)}") + st.error(f"❌ {str(_('errors.file_loading', 'Erreur lors du chargement des fiches :'))} {str(e)}") return dictionnaire_fiches @@ -56,7 +57,7 @@ def rechercher_tickets_gitea(fiche_selectionnee): try: issues = reponse.json() except Exception as e: - st.error(f"Erreur de décodage JSON : {e}") + st.error(f"{str(_('errors.json_decode', 'Erreur de décodage JSON :'))} {e}") return [] correspondances = charger_fiches_et_labels() @@ -86,7 +87,7 @@ def get_labels_existants(): try: return {label['name']: label['id'] for label in reponse.json()} except Exception as e: - st.error(f"Erreur de parsing des labels : {e}") + st.error(f"{str(_('errors.label_parsing', 'Erreur de parsing des labels :'))} {e}") return {} @@ -113,6 +114,6 @@ def creer_ticket_gitea(titre, corps, labels): issue_url = reponse.json().get("html_url", "") if issue_url: - st.success(f"Ticket créé ! [Voir le ticket]({issue_url})") + st.success(f"{str(_('pages.fiches.tickets.created_success', 'Ticket créé !'))} [Voir le ticket]({issue_url})") else: - st.success("Ticket créé avec succès.") + st.success(str(_('pages.fiches.tickets.created', 'Ticket créé avec succès.'))) diff --git a/app/fiches/utils/tickets/creation.py b/app/fiches/utils/tickets/creation.py index eed99a5..0035a27 100644 --- a/app/fiches/utils/tickets/creation.py +++ b/app/fiches/utils/tickets/creation.py @@ -3,6 +3,7 @@ import re import base64 import streamlit as st +from utils.translations import _ from .core import charger_fiches_et_labels, construire_corps_ticket_markdown, creer_ticket_gitea, get_labels_existants, nettoyer_labels from config import ENV import requests @@ -38,7 +39,7 @@ def generer_labels(fiche_selectionnee): if len(cible["operations"]) == 1: labels.append(cible["operations"][0]) elif len(cible["operations"]) > 1: - selected_ops = st.multiselect("Labels opération à associer", + selected_ops = st.multiselect(str(_("pages.fiches.tickets.contribution_type", "Labels opération à associer")), cible["operations"], default=cible["operations"]) @@ -52,14 +53,14 @@ def creer_champs_formulaire(sections, fiche_selectionnee): for section, aide in sections.items(): if "Type de contribution" in section: options = sorted(set(re.findall(r"- \[.\] (.+)", aide))) - if "Autre" not in options: - options.append("Autre") - choix = st.radio("Type de contribution", options) - reponses[section] = st.text_input("Précisez", "") if choix == "Autre" else choix + if str(_("pages.fiches.tickets.other", "Autre")) not in options: + options.append(str(_("pages.fiches.tickets.other", "Autre"))) + choix = st.radio(str(_("pages.fiches.tickets.contribution_type", "Type de contribution")), options) + reponses[section] = st.text_input(str(_("pages.fiches.tickets.specify", "Précisez")), "") if choix == str(_("pages.fiches.tickets.other", "Autre")) else choix elif "Fiche concernée" in section: url_fiche = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/{fiche_selectionnee.replace(' ', '%20')}" reponses[section] = url_fiche - st.text_input("Fiche concernée", value=url_fiche, disabled=True) + st.text_input(str(_("pages.fiches.tickets.concerned_card", "Fiche concernée")), value=url_fiche, disabled=True) elif "Sujet de la proposition" in section: reponses[section] = st.text_input(section, help=aide) else: @@ -71,9 +72,9 @@ def creer_champs_formulaire(sections, fiche_selectionnee): def afficher_controles_formulaire(): """Affiche les boutons de contrôle du formulaire.""" col1, col2 = st.columns(2) - if col1.button("Prévisualiser le ticket"): + if col1.button(str(_("pages.fiches.tickets.preview", "Prévisualiser le ticket"))): st.session_state.previsualiser = True - if col2.button("Annuler"): + if col2.button(str(_("pages.fiches.tickets.cancel", "Annuler"))): st.session_state.previsualiser = False st.rerun() @@ -83,7 +84,7 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible): if not st.session_state.get("previsualiser", False): return - st.subheader("Prévisualisation du ticket") + st.subheader(str(_("pages.fiches.tickets.preview_title", "Prévisualisation du ticket"))) for section, texte in reponses.items(): st.markdown(f"#### {section}") st.code(texte, language="markdown") @@ -91,9 +92,9 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible): titre_ticket = reponses.get("Sujet de la proposition", "").strip() or "Ticket FabNum" final_labels = nettoyer_labels(labels + selected_ops + ([cible["item"]] if cible else [])) - st.markdown(f"**Résumé :**\n- **Titre** : `{titre_ticket}`\n- **Labels** : `{', '.join(final_labels)}`") + st.markdown(f"**{str(_('pages.fiches.tickets.summary', 'Résumé'))} :**\n- **{str(_('pages.fiches.tickets.title', 'Titre'))}** : `{titre_ticket}`\n- **{str(_('pages.fiches.tickets.labels', 'Labels'))}** : `{', '.join(final_labels)}`") - if st.button("Confirmer la création du ticket"): + if st.button(str(_("pages.fiches.tickets.confirm", "Confirmer la création du ticket"))): labels_existants = get_labels_existants() labels_ids = [labels_existants[l] for l in final_labels if l in labels_existants] if "Backlog" in labels_existants: @@ -103,16 +104,16 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible): creer_ticket_gitea(titre_ticket, corps, labels_ids) st.session_state.previsualiser = False - st.success("Ticket créé et formulaire vidé.") + st.success(str(_("pages.fiches.tickets.created", "Ticket créé et formulaire vidé."))) def formulaire_creation_ticket_dynamique(fiche_selectionnee): """Fonction principale pour le formulaire de création de ticket.""" - with st.expander("Créer un nouveau ticket lié à cette fiche", expanded=False): + with st.expander(str(_("pages.fiches.tickets.create_new", "Créer un nouveau ticket lié à cette fiche")), expanded=False): # Chargement et vérification du modèle contenu_modele = charger_modele_ticket() if not contenu_modele: - st.error("Impossible de charger le modèle de ticket.") + st.error(str(_("pages.fiches.tickets.model_load_error", "Impossible de charger le modèle de ticket."))) return # Traitement du modèle et génération du formulaire @@ -135,5 +136,5 @@ def charger_modele_ticket(): r.raise_for_status() return base64.b64decode(r.json().get("content", "")).decode("utf-8") except Exception as e: - st.error(f"Erreur chargement modèle : {e}") + st.error(f"{str(_('pages.fiches.tickets.model_error', 'Erreur chargement modèle :'))} {e}") return "" diff --git a/app/fiches/utils/tickets/display.py b/app/fiches/utils/tickets/display.py index 16b3573..773921c 100644 --- a/app/fiches/utils/tickets/display.py +++ b/app/fiches/utils/tickets/display.py @@ -5,23 +5,28 @@ import html import re from collections import defaultdict from dateutil import parser +from utils.translations import _ from .core import rechercher_tickets_gitea def extraire_statut_par_label(ticket): labels = [label.get('name', '') for label in ticket.get('labels', [])] - for statut in ["Backlog", "En attente de traitement", "En cours", "Terminés", "Non retenus"]: + for statut in ["Backlog", + str(_("pages.fiches.tickets.status.awaiting", "En attente de traitement")), + str(_("pages.fiches.tickets.status.in_progress", "En cours")), + str(_("pages.fiches.tickets.status.completed", "Terminés")), + str(_("pages.fiches.tickets.status.rejected", "Non retenus"))]: if statut in labels: return statut - return "Autres" + return str(_("pages.fiches.tickets.status.others", "Autres")) def afficher_tickets_par_fiche(tickets): if not tickets: - st.info("Aucun ticket lié à cette fiche.") + st.info(str(_("pages.fiches.tickets.no_linked_tickets", "Aucun ticket lié à cette fiche."))) return - st.markdown("**Tickets associés à cette fiche**") + st.markdown(str(_("pages.fiches.tickets.associated_tickets", "**Tickets associés à cette fiche**"))) tickets_groupes = defaultdict(list) for ticket in tickets: statut = extraire_statut_par_label(ticket) @@ -29,9 +34,15 @@ def afficher_tickets_par_fiche(tickets): nb_backlogs = len(tickets_groupes["Backlog"]) if nb_backlogs: - st.info(f"⤇ {nb_backlogs} ticket(s) en attente de modération ne sont pas affichés.") + st.info(f"⤇ {nb_backlogs} {str(_('pages.fiches.tickets.moderation_notice', 'ticket(s) en attente de modération ne sont pas affichés.'))}") - ordre_statuts = ["En attente de traitement", "En cours", "Terminés", "Non retenus", "Autres"] + ordre_statuts = [ + str(_("pages.fiches.tickets.status.awaiting", "En attente de traitement")), + str(_("pages.fiches.tickets.status.in_progress", "En cours")), + str(_("pages.fiches.tickets.status.completed", "Terminés")), + str(_("pages.fiches.tickets.status.rejected", "Non retenus")), + str(_("pages.fiches.tickets.status.others", "Autres")) + ] for statut in ordre_statuts: if tickets_groupes[statut]: with st.expander(f"{statut} ({len(tickets_groupes[statut])})", expanded=(statut == "En cours")): @@ -50,14 +61,14 @@ def recuperer_commentaires_ticket(issue_index): response.raise_for_status() return response.json() except Exception as e: - st.error(f"Erreur lors de la récupération des commentaires : {e}") + st.error(f"{str(_('pages.fiches.tickets.comment_error', 'Erreur lors de la récupération des commentaires :'))} {e}") return [] def afficher_carte_ticket(ticket): - titre = ticket.get("title", "Sans titre") + titre = ticket.get("title", str(_("pages.fiches.tickets.no_title", "Sans titre"))) url = ticket.get("html_url", "") - user = ticket.get("user", {}).get("login", "inconnu") + user = ticket.get("user", {}).get("login", str(_("pages.fiches.tickets.unknown", "inconnu"))) created = ticket.get("created_at", "") updated = ticket.get("updated_at", "") body = ticket.get("body", "") @@ -75,12 +86,12 @@ def afficher_carte_ticket(ticket): return "?" date_created_str = format_date(created) - maj_info = f"(MAJ {format_date(updated)})" if updated and updated != created else "" + maj_info = f"({str(_('pages.fiches.tickets.updated', 'MAJ'))} {format_date(updated)})" if updated and updated != created else "" commentaires = recuperer_commentaires_ticket(ticket.get("number")) commentaires_html = "" for commentaire in commentaires: - auteur = html.escape(commentaire.get('user', {}).get('login', 'inconnu')) + auteur = html.escape(commentaire.get('user', {}).get('login', str(_("pages.fiches.tickets.unknown", "inconnu")))) contenu = html.escape(commentaire.get('body', '')) date = format_date(commentaire.get('created_at', '')) commentaires_html += f""" @@ -94,12 +105,12 @@ def afficher_carte_ticket(ticket): st.markdown(f"""

{titre}

-

Ouvert par {html.escape(user)} le {date_created_str} {maj_info}

-

Sujet : {html.escape(sujet)}

-

Labels : {' • '.join(labels) if labels else 'aucun'}

+

{str(_("pages.fiches.tickets.opened_by", "Ouvert par"))} {html.escape(user)} {str(_("pages.fiches.tickets.on_date", "le"))} {date_created_str} {maj_info}

+

{str(_("pages.fiches.tickets.subject_label", "Sujet"))} : {html.escape(sujet)}

+

Labels : {' • '.join(labels) if labels else str(_("pages.fiches.tickets.no_labels", "aucun"))}

""", unsafe_allow_html=True) st.markdown(body, unsafe_allow_html=False) st.markdown("---") - st.markdown("**Commentaire(s) :**") - st.markdown(commentaires_html or "Aucun commentaire.", unsafe_allow_html=True) + st.markdown(str(_("pages.fiches.tickets.comments", "**Commentaire(s) :**"))) + st.markdown(commentaires_html or str(_("pages.fiches.tickets.no_comments", "Aucun commentaire.")), unsafe_allow_html=True) diff --git a/app/personnalisation/ajout.py b/app/personnalisation/ajout.py index bf3650d..3686a0b 100644 --- a/app/personnalisation/ajout.py +++ b/app/personnalisation/ajout.py @@ -1,22 +1,23 @@ import streamlit as st +from utils.translations import _ def ajouter_produit(G): - st.markdown("## Ajouter un nouveau produit final") - new_prod = st.text_input("Nom du nouveau produit (unique)", key="new_prod") + st.markdown(f"## {str(_('pages.personnalisation.add_new_product', 'Ajouter un nouveau produit final'))}") + new_prod = st.text_input(str(_("pages.personnalisation.new_product_name", "Nom du nouveau produit (unique)")), key="new_prod") if new_prod: ops_dispo = sorted([ n for n, d in G.nodes(data=True) if d.get("niveau") == "10" and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n)) ]) - sel_new_op = st.selectbox("Opération d'assemblage (optionnelle)", ["-- Aucune --"] + ops_dispo, index=0) + sel_new_op = st.selectbox(str(_("pages.personnalisation.assembly_operation", "Opération d'assemblage (optionnelle)")), [str(_("pages.personnalisation.none", "-- Aucune --"))] + ops_dispo, index=0) niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"]) - sel_comps = st.multiselect("Composants à lier", options=niveau1) - if st.button("Créer le produit"): + sel_comps = st.multiselect(str(_("pages.personnalisation.components_to_link", "Composants à lier")), options=niveau1) + if st.button(str(_("pages.personnalisation.create_product", "Créer le produit"))): G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod) - if sel_new_op != "-- Aucune --": + if sel_new_op != str(_("pages.personnalisation.none", "-- Aucune --")): G.add_edge(new_prod, sel_new_op) for comp in sel_comps: G.add_edge(new_prod, comp) - st.success(f"{new_prod} ajouté.") + st.success(f"{new_prod} {str(_('pages.personnalisation.added', 'ajouté'))}") return G diff --git a/app/personnalisation/interface.py b/app/personnalisation/interface.py index 53480fb..4e3573f 100644 --- a/app/personnalisation/interface.py +++ b/app/personnalisation/interface.py @@ -1,21 +1,24 @@ # interface.py – app/personnalisation import streamlit as st +from utils.translations import _ from .ajout import ajouter_produit from .modification import modifier_produit from .import_export import importer_exporter_graph def interface_personnalisation(G): - st.markdown("# Personnalisation des produits finaux") - with st.expander("Comment utiliser cet onglet ?", expanded=False): - st.markdown(""" - 1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit - 2. Donnez un nom à votre produit - 3. Sélectionnez une opération d'assemblage appropriée (si pertinent) - 4. Choisissez les composants qui constituent votre produit dans la liste proposée - 5. Sauvegardez votre configuration pour une réutilisation future - 6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés - """) + st.markdown(f"# {str(_('pages.personnalisation.title', 'Personnalisation des produits finaux'))}") + with st.expander(str(_("pages.personnalisation.help", "Comment utiliser cet onglet ?")), expanded=False): + st.markdown("\n".join([ + " " + line for line in _("pages.personnalisation.help_content", [ + "1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit", + "2. Donnez un nom à votre produit", + "3. Sélectionnez une opération d'assemblage appropriée (si pertinent)", + "4. Choisissez les composants qui constituent votre produit dans la liste proposée", + "5. Sauvegardez votre configuration pour une réutilisation future", + "6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés" + ]) + ])) st.markdown("---") G = ajouter_produit(G) diff --git a/app/personnalisation/modification.py b/app/personnalisation/modification.py index cb25ed8..35bc202 100644 --- a/app/personnalisation/modification.py +++ b/app/personnalisation/modification.py @@ -1,4 +1,5 @@ import streamlit as st +from utils.translations import _ def get_produits_personnalises(G): """Récupère la liste des produits personnalisés du niveau 0.""" @@ -7,7 +8,7 @@ def get_produits_personnalises(G): def supprimer_produit(G, prod): """Supprime un produit du graphe.""" G.remove_node(prod) - st.success(f"{prod} supprimé.") + st.success(f"{prod} {str(_('pages.personnalisation.deleted', 'supprimé'))}") st.session_state.pop("prod_sel", None) return G @@ -33,10 +34,11 @@ def get_composants_lies(G, prod): def mettre_a_jour_operations(G, prod, curr_ops, sel_op): """Met à jour les opérations liées au produit.""" + none_option = str(_("pages.personnalisation.none", "-- Aucune --")) for op in curr_ops: - if sel_op == "-- Aucune --" or op != sel_op: + if sel_op == none_option or op != sel_op: G.remove_edge(prod, op) - if sel_op != "-- Aucune --" and (not curr_ops or sel_op not in curr_ops): + if sel_op != none_option and (not curr_ops or sel_op not in curr_ops): G.add_edge(prod, sel_op) return G @@ -49,11 +51,11 @@ def mettre_a_jour_composants(G, prod, linked, nouveaux): return G def modifier_produit(G): - st.markdown("## Modifier un produit final ajouté") + st.markdown(f"## {str(_('pages.personnalisation.modify_product', 'Modifier un produit final ajouté'))}") # Sélection du produit à modifier produits0 = get_produits_personnalises(G) - sel_display = st.multiselect("Produits à modifier", options=produits0) + sel_display = st.multiselect(str(_("pages.personnalisation.products_to_modify", "Produits à modifier")), options=produits0) if not sel_display: return G @@ -62,24 +64,24 @@ def modifier_produit(G): prod = sel_display[0] # Suppression du produit si demandé - if st.button(f"Supprimer {prod}"): + if st.button(f"{str(_('pages.personnalisation.delete', 'Supprimer'))} {prod}"): return supprimer_produit(G, prod) # Gestion des opérations d'assemblage ops_dispo = get_operations_disponibles(G) curr_ops = get_operations_actuelles(G, prod) default_idx = ops_dispo.index(curr_ops[0]) + 1 if curr_ops and curr_ops[0] in ops_dispo else 0 - sel_op = st.selectbox("Opération d'assemblage liée", ["-- Aucune --"] + ops_dispo, index=default_idx) + sel_op = st.selectbox(str(_("pages.personnalisation.linked_assembly_operation", "Opération d'assemblage liée")), [str(_("pages.personnalisation.none", "-- Aucune --"))] + ops_dispo, index=default_idx) # Gestion des composants niveau1 = get_composants_niveau1(G) linked = get_composants_lies(G, prod) - nouveaux = st.multiselect(f"Composants liés à {prod}", options=niveau1, default=linked) + nouveaux = st.multiselect(f"{str(_('pages.personnalisation.components_linked_to', 'Composants liés à'))} {prod}", options=niveau1, default=linked) # Mise à jour des liens si demandé - if st.button(f"Mettre à jour {prod}"): + if st.button(f"{str(_('pages.personnalisation.update', 'Mettre à jour'))} {prod}"): G = mettre_a_jour_operations(G, prod, curr_ops, sel_op) G = mettre_a_jour_composants(G, prod, linked, nouveaux) - st.success(f"{prod} mis à jour.") + st.success(f"{prod} {str(_('pages.personnalisation.updated', 'mis à jour'))}") return G diff --git a/app/visualisations/graphes.py b/app/visualisations/graphes.py index 2bf2aad..9c30dd4 100644 --- a/app/visualisations/graphes.py +++ b/app/visualisations/graphes.py @@ -3,14 +3,31 @@ import altair as alt import numpy as np from collections import Counter import pandas as pd +from utils.translations import _ def afficher_graphique_altair(df): - ordre_personnalise = ['Assemblage', 'Fabrication', 'Traitement', 'Extraction'] - categories = [cat for cat in ordre_personnalise if cat in df['categorie'].unique()] - for cat in categories: - st.markdown(f"### {cat}") - df_cat = df[df['categorie'] == cat].copy() + # Définir les catégories originales (en français) et leur ordre + categories_fr = ["Assemblage", "Fabrication", "Traitement", "Extraction"] + + # Créer un dictionnaire de mappage entre les catégories originales et leurs traductions + mappage_categories = { + "Assemblage": str(_("pages.visualisations.categories.assembly", "Assemblage")), + "Fabrication": str(_("pages.visualisations.categories.manufacturing", "Fabrication")), + "Traitement": str(_("pages.visualisations.categories.processing", "Traitement")), + "Extraction": str(_("pages.visualisations.categories.extraction", "Extraction")) + } + + # Filtrer les catégories qui existent dans les données + categories_fr_filtrees = [cat for cat in categories_fr if cat in df['categorie'].unique()] + + # Parcourir les catégories dans l'ordre défini + for cat_fr in categories_fr_filtrees: + # Obtenir le nom traduit de la catégorie pour l'affichage + cat_traduit = mappage_categories[cat_fr] + st.markdown(f"### {cat_traduit}") + # Mais filtrer sur le nom original dans les données + df_cat = df[df['categorie'] == cat_fr].copy() coord_pairs = list(zip(df_cat['ihh_pays'].round(1), df_cat['ihh_acteurs'].round(1))) counts = Counter(coord_pairs) @@ -36,8 +53,8 @@ def afficher_graphique_altair(df): df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5 base = alt.Chart(df_cat).encode( - x=alt.X('ihh_pays:Q', title='IHH Pays (%)'), - y=alt.Y('ihh_acteurs:Q', title='IHH Acteurs (%)'), + x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries", "IHH Pays (%)"))), + y=alt.Y('ihh_acteurs:Q', title=str(_("pages.visualisations.axis_titles.ihh_actors", "IHH Acteurs (%)"))), size=alt.Size('criticite_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None), color=alt.Color('criticite_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])) ) @@ -66,7 +83,7 @@ def afficher_graphique_altair(df): chart = (points + lines + labels + hline_15 + hline_25 + hline_100 + vline_15 + vline_25 + vline_100).properties( width=500, height=400, - title=f"Concentration et criticité – {cat}" + title=str(_("pages.visualisations.chart_titles.concentration_criticality", "Concentration et criticité – {0}")).format(cat_traduit) ).interactive() st.altair_chart(chart, use_container_width=True) @@ -74,7 +91,7 @@ def afficher_graphique_altair(df): def creer_graphes(donnees): if not donnees: - st.warning("Aucune donnée à afficher.") + st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à afficher."))) return try: @@ -105,8 +122,8 @@ def creer_graphes(donnees): df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5 base = alt.Chart(df).encode( - x=alt.X('ihh_extraction:Q', title='IHH Extraction (%)'), - y=alt.Y('ihh_reserves:Q', title='IHH Réserves (%)'), + x=alt.X('ihh_extraction:Q', title=str(_("pages.visualisations.axis_titles.ihh_extraction", "IHH Extraction (%)"))), + y=alt.Y('ihh_reserves:Q', title=str(_("pages.visualisations.axis_titles.ihh_reserves", "IHH Réserves (%)"))), size=alt.Size('ivc_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None), color=alt.Color('ivc_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])), tooltip=['nom:N', 'ivc:Q', 'ihh_extraction:Q', 'ihh_reserves:Q'] @@ -136,13 +153,13 @@ def creer_graphes(donnees): chart = (points + lines + labels + hline_15 + hline_25 + hline_100 + vline_15 + vline_25 + vline_100).properties( width=600, height=500, - title="Concentration des ressources critiques vs vulnérabilité IVC" + title=str(_("pages.visualisations.chart_titles.concentration_resources", "Concentration des ressources critiques vs vulnérabilité IVC")) ).interactive() st.altair_chart(chart, use_container_width=True) except Exception as e: - st.error(f"Erreur lors de la création du graphique : {e}") + st.error(f"{str(_('errors.graph_creation_error', 'Erreur lors de la création du graphique :'))} {e}") def lancer_visualisation_ihh_criticite(graph): @@ -156,11 +173,11 @@ def lancer_visualisation_ihh_criticite(graph): df = recuperer_donnees(graph, noeuds) if df.empty: - st.warning("Aucune donnée à visualiser.") + st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à visualiser."))) else: afficher_graphique_altair(df) except Exception as e: - st.error(f"Erreur dans la visualisation IHH vs Criticité : {e}") + st.error(f"{str(_('errors.ihh_criticality_error', 'Erreur dans la visualisation IHH vs Criticité :'))} {e}") def lancer_visualisation_ihh_ivc(graph): @@ -175,4 +192,4 @@ def lancer_visualisation_ihh_ivc(graph): data = recuperer_donnees_2(graph, noeuds_niveau_2) creer_graphes(data) except Exception as e: - st.error(f"Erreur dans la visualisation IHH vs IVC : {e}") + st.error(f"{str(_('errors.ihh_ivc_error', 'Erreur dans la visualisation IHH vs IVC :'))} {e}") diff --git a/app/visualisations/interface.py b/app/visualisations/interface.py index 266dff7..8f4dc0a 100644 --- a/app/visualisations/interface.py +++ b/app/visualisations/interface.py @@ -1,4 +1,5 @@ import streamlit as st +from utils.translations import _ from .graphes import ( lancer_visualisation_ihh_criticite, @@ -7,39 +8,39 @@ from .graphes import ( def interface_visualisations(G_temp, G_temp_ivc): - st.markdown("# Analyse du graphe") - with st.expander("Comment utiliser cet onglet ?", expanded=False): - st.markdown(""" - 1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH) - 2. Analysez sa relation avec la criticité moyenne des minerais ou leur Indice de Vulnérabilité Concurrentielle (IVC) - 3. Zoomer dans les graphes pour mieux découvrir les informations - - Il est important de se rappeler que l'IHH a deux seuils : - * en-dessous de 15, la concentration est considérée comme étant faible - * au-dessus de 25, elle est considérée comme étant forte - - Ainsi plus le positionnement d'un point est en haut à droite des graphiques, plus les risques sont élevés. - Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils. - """) + st.markdown(f"# {str(_('pages.visualisations.title', 'Analyse du graphe'))}") + with st.expander(str(_("pages.visualisations.help", "Comment utiliser cet onglet ?")), expanded=False): + st.markdown("\n".join(_("pages.visualisations.help_content", [ + "1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH)", + "2. Analysez sa relation avec la criticité moyenne des minerais ou leur Indice de Vulnérabilité Concurrentielle (IVC)", + "3. Zoomer dans les graphes pour mieux découvrir les informations", + "", + "Il est important de se rappeler que l'IHH a deux seuils :", + "* en-dessous de 15, la concentration est considérée comme étant faible", + "* au-dessus de 25, elle est considérée comme étant forte", + "", + "Ainsi plus le positionnement d'un point est en haut à droite des graphiques, plus les risques sont élevés.", + "Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils." + ]))) st.markdown("---") - st.markdown("""## Indice de Herfindahl-Hirschmann - IHH vs Criticité + st.markdown(f"""## {str(_("pages.visualisations.ihh_criticality", "Indice de Herfindahl-Hirschmann - IHH vs Criticité"))} -La taille des points donne l'indication de la criticité de substituabilité du minerai. +{str(_("pages.visualisations.ihh_criticality_desc", "La taille des points donne l'indication de la criticité de substituabilité du minerai."))} """) - if st.button("Lancer", key="btn_ihh_criticite"): + if st.button(str(_("buttons.run", "Lancer")), key="btn_ihh_criticite"): try: lancer_visualisation_ihh_criticite(G_temp) except Exception as e: - st.error(f"Erreur dans la visualisation IHH vs Criticité : {e}") + st.error(f"{str(_('errors.ihh_criticality_error', 'Erreur dans la visualisation IHH vs Criticité :'))} {e}") - st.markdown("""## Indice de Herfindahl-Hirschmann - IHH vs IVC + st.markdown(f"""## {str(_("pages.visualisations.ihh_ivc", "Indice de Herfindahl-Hirschmann - IHH vs IVC"))} -La taille des points donne l'indication de la criticité concurrentielle du minerai. +{str(_("pages.visualisations.ihh_ivc_desc", "La taille des points donne l'indication de la criticité concurrentielle du minerai."))} """) - if st.button("Lancer", key="btn_ihh_ivc"): + if st.button(str(_("buttons.run", "Lancer")), key="btn_ihh_ivc"): try: lancer_visualisation_ihh_ivc(G_temp_ivc) except Exception as e: - st.error(f"Erreur dans la visualisation IHH vs IVC : {e}") + st.error(f"{str(_('errors.ihh_ivc_error', 'Erreur dans la visualisation IHH vs IVC :'))} {e}") diff --git a/assets/locales/en.json b/assets/locales/en.json new file mode 100644 index 0000000..3e07dc1 --- /dev/null +++ b/assets/locales/en.json @@ -0,0 +1,276 @@ +{ + "app": { + "title": "Fabnum – Chain Analysis", + "description": "Ecosystem exploration and vulnerability identification.", + "dev_mode": "You are in the development environment." + }, + "header": { + "title": "FabNum - Digital Manufacturing Chain", + "subtitle": "Ecosystem exploration and vulnerability identification." + }, + "footer": { + "copyright": "Fabnum © 2025", + "contact": "Contact", + "license": "License", + "license_text": "CC BY-NC-ND", + "eco_note": "🌱 CO₂ calculations via", + "eco_provider": "The Green Web Foundation", + "powered_by": "🚀 Powered by", + "powered_by_name": "Streamlit" + }, + "sidebar": { + "menu": "Main Menu", + "navigation": "Main Navigation", + "theme": "Theme", + "theme_light": "Light", + "theme_dark": "Dark", + "theme_instructions_only": "Theme changes can only be made from the Instructions tab.", + "impact": "Environmental Impact", + "loading": "Loading..." + }, + "auth": { + "title": "Authentication", + "username": "Username_token", + "token": "Gitea Personal Access Token", + "login": "Login", + "logout": "Logout", + "logged_as": "Logged in as", + "error": "❌ Access denied.", + "gitea_error": "❌ Unable to verify user with Gitea.", + "success": "Successfully logged out." + }, + "navigation": { + "instructions": "Instructions", + "personnalisation": "Customization", + "analyse": "Analysis", + "visualisations": "Visualizations", + "fiches": "Cards" + }, + "pages": { + "instructions": { + "title": "Instructions" + }, + "personnalisation": { + "title": "Final Product Customization", + "help": "How to use this tab?", + "help_content": [ + "1. Click on \"Add a final product\" to create a new product", + "2. Give your product a name", + "3. Select an appropriate assembly operation (if relevant)", + "4. Choose the components that make up your product from the list provided", + "5. Save your configuration for future reuse", + "6. You will be able to modify or delete your custom products later" + ], + "add_new_product": "Add a new final product", + "new_product_name": "New product name (unique)", + "assembly_operation": "Assembly operation (optional)", + "none": "-- None --", + "components_to_link": "Components to link", + "create_product": "Create product", + "added": "added", + "modify_product": "Modify an added final product", + "products_to_modify": "Products to modify", + "delete": "Delete", + "linked_assembly_operation": "Linked assembly operation", + "components_linked_to": "Components linked to", + "update": "Update", + "updated": "updated", + "deleted": "deleted", + "save_restore_config": "Save or restore configuration", + "export_config": "Export configuration", + "download_json": "Download (JSON)", + "import_config": "Import a JSON configuration (max 100 KB)", + "file_too_large": "File too large (max 100 KB).", + "no_products_found": "No products found in the file.", + "select_products_to_restore": "Select products to restore", + "products_to_restore": "Products to restore", + "restore_selected": "Restore selected items", + "config_restored": "Partial configuration successfully restored.", + "import_error": "Import error:" + }, + "analyse": { + "title": "Graph Analysis", + "help": "How to use this tab?", + "help_content": [ + "1. Select the starting level (final product, component, or mineral)", + "2. Choose the desired destination level", + "3. Refine your selection by specifying either one or more specific minerals to target or specific items at each level (optional)", + "4. Define the analysis criteria by selecting the relevant vulnerability indices", + "5. Choose the index combination mode (AND/OR) according to your analysis needs", + "6. Explore the generated graph using zoom and panning controls; you can switch to full screen mode for the graph" + ], + "selection_nodes": "Selection of start and end nodes", + "select_level": "-- Select a level --", + "start_level": "Start level", + "end_level": "End level", + "select_minerals": "Select one or more minerals", + "filter_by_minerals": "Filter by minerals (optional)", + "fine_selection": "Fine selection of items", + "filter_start_nodes": "Filter by start nodes (optional)", + "filter_end_nodes": "Filter by end nodes (optional)", + "vulnerability_filters": "Selection of filters to identify vulnerabilities", + "filter_ics": "Filter paths containing at least one critical mineral for a component (ICS > 66%)", + "filter_ivc": "Filter paths containing at least one critical mineral in relation to sectoral competition (IVC > 30)", + "filter_ihh": "Filter paths containing at least one critical operation in relation to geographical or industrial concentration (IHH countries or actors > 25)", + "apply_ihh_filter": "Apply IHH filter on:", + "countries": "Countries", + "actors": "Actors", + "filter_isg": "Filter paths containing an unstable country (ISG ≥ 60)", + "filter_logic": "Filter logic", + "or": "OR", + "and": "AND", + "run_analysis": "Run analysis", + "sankey": { + "no_paths": "No paths found for the specified criteria.", + "no_matching_paths": "No paths match the criteria.", + "filtered_hierarchy": "Hierarchy filtered by levels and nodes", + "download_dot": "Download filtered DOT file", + "relation": "Relation" + } + }, + "visualisations": { + "title": "Graph Analysis", + "help": "How to use this tab?", + "help_content": [ + "1. Explore the graphs presenting the Herfindahl-Hirschmann Index (IHH)", + "2. Analyze its relationship with the average criticality of minerals or their Competitive Vulnerability Index (IVC)", + "3. Zoom in on the graphs to better discover the information", + "", + "It is important to remember that the IHH has two thresholds:", + "* below 15, concentration is considered to be low", + "* above 25, it is considered to be high", + "", + "Thus, the higher a point is positioned in the top right of the graphs, the higher the risks.", + "The graphs present 2 horizontal and vertical lines to mark these thresholds." + ], + "ihh_criticality": "Herfindahl-Hirschmann Index - IHH vs Criticality", + "ihh_criticality_desc": "The size of the points indicates the substitutability criticality of the mineral.", + "ihh_ivc": "Herfindahl-Hirschmann Index - IHH vs IVC", + "ihh_ivc_desc": "The size of the points indicates the competitive criticality of the mineral.", + "launch": "Launch", + "no_data": "No data to display.", + "categories": { + "assembly": "Assembly", + "manufacturing": "Manufacturing", + "processing": "Processing", + "extraction": "Extraction" + }, + "axis_titles": { + "ihh_countries": "IHH Countries (%)", + "ihh_actors": "IHH Actors (%)", + "ihh_extraction": "IHH Extraction (%)", + "ihh_reserves": "IHH Reserves (%)" + }, + "chart_titles": { + "concentration_criticality": "Concentration and Criticality – {0}", + "concentration_resources": "Concentration of Critical Resources vs IVC Vulnerability" + } + }, + "fiches": { + "title": "Card Discovery", + "help": "How to use this tab?", + "help_content": [ + "1. Browse the list of available cards by category", + "2. Select a card to display its full content", + "3. Consult detailed data, graphs, and additional analyses", + "4. Use this information to deepen your understanding of the identified vulnerabilities", + "", + "The categories are as follows:", + "* Assembly: operation of assembling final products from components", + "* Related: various operations necessary to manufacture digital technology, but not directly entering its composition", + "* Criticalities: indices used to identify and evaluate vulnerabilities", + "* Manufacturing: operation of manufacturing components from minerals", + "* Mineral: description and operations of extraction and processing of minerals" + ], + "no_files": "No cards available at the moment.", + "choose_category": "Choose a card category", + "select_folder": "-- Select a folder --", + "choose_file": "Choose a card", + "select_file": "-- Select a card --", + "loading_error": "Error loading the card:", + "download_pdf": "Download this card as PDF", + "pdf_unavailable": "The PDF file for this card is not available.", + "ticket_management": "Ticket management for this card", + "tickets": { + "create_new": "Create a new ticket linked to this card", + "model_load_error": "Unable to load the ticket template.", + "contribution_type": "Contribution type", + "specify": "Specify", + "other": "Other", + "concerned_card": "Concerned card", + "subject": "Subject of the proposal", + "preview": "Preview ticket", + "cancel": "Cancel", + "preview_title": "Ticket preview", + "summary": "Summary", + "title": "Title", + "labels": "Labels", + "confirm": "Confirm ticket creation", + "created": "Ticket created and form cleared.", + "model_error": "Template loading error:", + "no_linked_tickets": "No tickets linked to this card.", + "associated_tickets": "Tickets associated with this card", + "moderation_notice": "ticket(s) awaiting moderation are not displayed.", + "status": { + "awaiting": "Awaiting processing", + "in_progress": "In progress", + "completed": "Completed", + "rejected": "Rejected", + "others": "Others" + }, + "no_title": "No title", + "unknown": "unknown", + "subject_label": "Subject", + "no_labels": "none", + "comments": "Comment(s):", + "no_comments": "No comments.", + "comment_error": "Error retrieving comments:", + "opened_by": "Opened by", + "on_date": "on", + "updated": "UPDATED" + } + } + }, + "node_levels": { + "0": "Final product", + "1": "Component", + "2": "Mineral", + "10": "Operation", + "11": "Operation country", + "12": "Operation actor", + "99": "Geographic country" + }, + "errors": { + "log_read_error": "Log reading error:", + "graph_preview_error": "Graph preview error:", + "graph_creation_error": "Error creating the graph:", + "ihh_criticality_error": "Error in IHH vs Criticality visualization:", + "ihh_ivc_error": "Error in IHH vs IVC visualization:", + "comment_fetch_error": "Error retrieving comments:", + "template_load_error": "Template loading error:", + "import_error": "Import error:" + }, + "buttons": { + "download": "Download", + "run": "Run", + "save": "Save", + "cancel": "Cancel", + "confirm": "Confirm", + "filter": "Filter", + "search": "Search", + "create": "Create", + "update": "Update", + "delete": "Delete", + "preview": "Preview", + "export": "Export", + "import": "Import", + "restore": "Restore", + "browse_files": "Browse files" + }, + "ui": { + "file_uploader": { + "drag_drop_here": "Drag and drop file here", + "size_limit": "100 KB limit per file • JSON" + } + } +} \ No newline at end of file diff --git a/assets/locales/fr.json b/assets/locales/fr.json new file mode 100644 index 0000000..a662a95 --- /dev/null +++ b/assets/locales/fr.json @@ -0,0 +1,276 @@ +{ + "app": { + "title": "Fabnum – Analyse de chaîne", + "description": "Parcours de l'écosystème et identification des vulnérabilités.", + "dev_mode": "Vous êtes dans l'environnement de développement." + }, + "header": { + "title": "FabNum - Chaîne de fabrication du numérique", + "subtitle": "Parcours de l'écosystème et identification des vulnérabilités." + }, + "footer": { + "copyright": "Fabnum © 2025", + "contact": "Contact", + "license": "Licence", + "license_text": "CC BY-NC-ND", + "eco_note": "🌱 Calculs CO₂ via", + "eco_provider": "The Green Web Foundation", + "powered_by": "🚀 Propulsé par", + "powered_by_name": "Streamlit" + }, + "sidebar": { + "menu": "Menu principal", + "navigation": "Navigation principale", + "theme": "Thème", + "theme_light": "Clair", + "theme_dark": "Sombre", + "theme_instructions_only": "Le changement de thème ne peut se faire que depuis l'onglet Instructions.", + "impact": "Impact environnemental", + "loading": "Chargement en cours…" + }, + "auth": { + "title": "Authentification", + "username": "Identifiant_token", + "token": "Token d'accès personnel Gitea", + "login": "Se connecter", + "logout": "Se déconnecter", + "logged_as": "Connecté en tant que", + "error": "❌ Accès refusé.", + "gitea_error": "❌ Impossible de vérifier l'utilisateur auprès de Gitea.", + "success": "Déconnecté avec succès." + }, + "navigation": { + "instructions": "Instructions", + "personnalisation": "Personnalisation", + "analyse": "Analyse", + "visualisations": "Visualisations", + "fiches": "Fiches" + }, + "pages": { + "instructions": { + "title": "Instructions" + }, + "personnalisation": { + "title": "Personnalisation des produits finaux", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit", + "2. Donnez un nom à votre produit", + "3. Sélectionnez une opération d'assemblage appropriée (si pertinent)", + "4. Choisissez les composants qui constituent votre produit dans la liste proposée", + "5. Sauvegardez votre configuration pour une réutilisation future", + "6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés" + ], + "add_new_product": "Ajouter un nouveau produit final", + "new_product_name": "Nom du nouveau produit (unique)", + "assembly_operation": "Opération d'assemblage (optionnelle)", + "none": "-- Aucune --", + "components_to_link": "Composants à lier", + "create_product": "Créer le produit", + "added": "ajouté", + "modify_product": "Modifier un produit final ajouté", + "products_to_modify": "Produits à modifier", + "delete": "Supprimer", + "linked_assembly_operation": "Opération d'assemblage liée", + "components_linked_to": "Composants liés à", + "update": "Mettre à jour", + "updated": "mis à jour", + "deleted": "supprimé", + "save_restore_config": "Sauvegarder ou restaurer la configuration", + "export_config": "Exporter configuration", + "download_json": "Télécharger (JSON)", + "import_config": "Importer une configuration JSON (max 100 Ko)", + "file_too_large": "Fichier trop volumineux (max 100 Ko).", + "no_products_found": "Aucun produit trouvé dans le fichier.", + "select_products_to_restore": "Sélection des produits à restaurer", + "products_to_restore": "Produits à restaurer", + "restore_selected": "Restaurer les éléments sélectionnés", + "config_restored": "Configuration partielle restaurée avec succès.", + "import_error": "Erreur d'import :" + }, + "analyse": { + "title": "Analyse du graphe", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Sélectionnez le niveau de départ (produit final, composant ou minerai)", + "2. Choisissez le niveau d'arrivée souhaité", + "3. Affinez votre sélection en spécifiant soit un ou des minerais à cibler spécifiquement ou des items précis à chaque niveau (optionnel)", + "4. Définissez les critères d'analyse en sélectionnant les indices de vulnérabilité pertinents", + "5. Choisissez le mode de combinaison des indices (ET/OU) selon votre besoin d'analyse", + "6. Explorez le graphique généré en utilisant les contrôles de zoom et de déplacement ; vous pouvez basculer en mode plein écran pour le graphe" + ], + "selection_nodes": "Sélection des nœuds de départ et d'arrivée", + "select_level": "-- Sélectionner un niveau --", + "start_level": "Niveau de départ", + "end_level": "Niveau d'arrivée", + "select_minerals": "Sélectionner un ou plusieurs minerais", + "filter_by_minerals": "Filtrer par minerais (optionnel)", + "fine_selection": "Sélection fine des items", + "filter_start_nodes": "Filtrer par noeuds de départ (optionnel)", + "filter_end_nodes": "Filtrer par noeuds d'arrivée (optionnel)", + "vulnerability_filters": "Sélection des filtres pour identifier les vulnérabilités", + "filter_ics": "Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)", + "filter_ivc": "Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)", + "filter_ihh": "Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)", + "apply_ihh_filter": "Appliquer le filtre IHH sur :", + "countries": "Pays", + "actors": "Acteurs", + "filter_isg": "Filtrer les chemins contenant un pays instable (ISG ≥ 60)", + "filter_logic": "Logique de filtrage", + "or": "OU", + "and": "ET", + "run_analysis": "Lancer l'analyse", + "sankey": { + "no_paths": "Aucun chemin trouvé pour les critères spécifiés.", + "no_matching_paths": "Aucun chemin ne correspond aux critères.", + "filtered_hierarchy": "Hiérarchie filtrée par niveaux et noeuds", + "download_dot": "Télécharger le fichier DOT filtré", + "relation": "Relation" + } + }, + "visualisations": { + "title": "Analyse du graphe", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH)", + "2. Analysez sa relation avec la criticité moyenne des minerais ou leur Indice de Vulnérabilité Concurrentielle (IVC)", + "3. Zoomer dans les graphes pour mieux découvrir les informations", + "", + "Il est important de se rappeler que l'IHH a deux seuils :", + "* en-dessous de 15, la concentration est considérée comme étant faible", + "* au-dessus de 25, elle est considérée comme étant forte", + "", + "Ainsi plus le positionnement d'un point est en haut à droite des graphiques, plus les risques sont élevés.", + "Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils." + ], + "ihh_criticality": "Indice de Herfindahl-Hirschmann - IHH vs Criticité", + "ihh_criticality_desc": "La taille des points donne l'indication de la criticité de substituabilité du minerai.", + "ihh_ivc": "Indice de Herfindahl-Hirschmann - IHH vs IVC", + "ihh_ivc_desc": "La taille des points donne l'indication de la criticité concurrentielle du minerai.", + "launch": "Lancer", + "no_data": "Aucune donnée à visualiser.", + "categories": { + "assembly": "Assemblage", + "manufacturing": "Fabrication", + "processing": "Traitement", + "extraction": "Extraction" + }, + "axis_titles": { + "ihh_countries": "IHH Pays (%)", + "ihh_actors": "IHH Acteurs (%)", + "ihh_extraction": "IHH Extraction (%)", + "ihh_reserves": "IHH Réserves (%)" + }, + "chart_titles": { + "concentration_criticality": "Concentration et criticité – {0}", + "concentration_resources": "Concentration des ressources critiques vs vulnérabilité IVC" + } + }, + "fiches": { + "title": "Découverte des fiches", + "help": "Comment utiliser cet onglet ?", + "help_content": [ + "1. Parcourez la liste des fiches disponibles par catégorie", + "2. Sélectionnez une fiche pour afficher son contenu complet", + "3. Consultez les données détaillées, graphiques et analyses supplémentaires", + "4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées", + "", + "Les catégories sont les suivantes :", + "* Assemblage : opération d'assemblage des produits finaux à partir des composants", + "* Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition", + "* Criticités : indices utilisés pour identifier et évaluer les vulnérabilités", + "* Fabrication : opération de fabrication des composants à partir de minerais", + "* Minerai : description et opérations d'extraction et de traitement des minerais" + ], + "no_files": "Aucune fiche disponible pour le moment.", + "choose_category": "Choisissez une catégorie de fiches", + "select_folder": "-- Sélectionner un dossier --", + "choose_file": "Choisissez une fiche", + "select_file": "-- Sélectionner une fiche --", + "loading_error": "Erreur lors du chargement de la fiche :", + "download_pdf": "Télécharger cette fiche en PDF", + "pdf_unavailable": "Le fichier PDF de cette fiche n'est pas disponible.", + "ticket_management": "Gestion des tickets pour cette fiche", + "tickets": { + "create_new": "Créer un nouveau ticket lié à cette fiche", + "model_load_error": "Impossible de charger le modèle de ticket.", + "contribution_type": "Type de contribution", + "specify": "Précisez", + "other": "Autre", + "concerned_card": "Fiche concernée", + "subject": "Sujet de la proposition", + "preview": "Prévisualiser le ticket", + "cancel": "Annuler", + "preview_title": "Prévisualisation du ticket", + "summary": "Résumé", + "title": "Titre", + "labels": "Labels", + "confirm": "Confirmer la création du ticket", + "created": "Ticket créé et formulaire vidé.", + "model_error": "Erreur chargement modèle :", + "no_linked_tickets": "Aucun ticket lié à cette fiche.", + "associated_tickets": "Tickets associés à cette fiche", + "moderation_notice": "ticket(s) en attente de modération ne sont pas affichés.", + "status": { + "awaiting": "En attente de traitement", + "in_progress": "En cours", + "completed": "Terminés", + "rejected": "Non retenus", + "others": "Autres" + }, + "no_title": "Sans titre", + "unknown": "inconnu", + "subject_label": "Sujet", + "no_labels": "aucun", + "comments": "Commentaire(s) :", + "no_comments": "Aucun commentaire.", + "comment_error": "Erreur lors de la récupération des commentaires :", + "opened_by": "Ouvert par", + "on_date": "le", + "updated": "MAJ" + } + } + }, + "node_levels": { + "0": "Produit final", + "1": "Composant", + "2": "Minerai", + "10": "Opération", + "11": "Pays d'opération", + "12": "Acteur d'opération", + "99": "Pays géographique" + }, + "errors": { + "log_read_error": "Erreur lecture log:", + "graph_preview_error": "Erreur de prévisualisation du graphe :", + "graph_creation_error": "Erreur lors de la création du graphique :", + "ihh_criticality_error": "Erreur dans la visualisation IHH vs Criticité :", + "ihh_ivc_error": "Erreur dans la visualisation IHH vs IVC :", + "comment_fetch_error": "Erreur lors de la récupération des commentaires :", + "template_load_error": "Erreur chargement modèle :", + "import_error": "Erreur d'import :" + }, + "buttons": { + "download": "Télécharger", + "run": "Lancer", + "save": "Enregistrer", + "cancel": "Annuler", + "confirm": "Confirmer", + "filter": "Filtrer", + "search": "Rechercher", + "create": "Créer", + "update": "Mettre à jour", + "delete": "Supprimer", + "preview": "Prévisualiser", + "export": "Exporter", + "import": "Importer", + "restore": "Restaurer", + "browse_files": "Parcourir les fichiers" + }, + "ui": { + "file_uploader": { + "drag_drop_here": "Glissez-déposez votre fichier ici", + "size_limit": "Limite 100 Ko par fichier • JSON" + } + } +} \ No newline at end of file diff --git a/assets/styles/base.css b/assets/styles/base.css index ac9aa78..dcbda92 100644 --- a/assets/styles/base.css +++ b/assets/styles/base.css @@ -7,11 +7,16 @@ visibility: hidden; } -body, html { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +body, +html { + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, + sans-serif; } -body, .stApp, .block-container { +body, +.stApp, +.block-container { background-color: var(--bg-color) !important; color: var(--text-color) !important; } @@ -47,14 +52,20 @@ body, .stApp, .block-container { margin-right: auto; } -section:not([data-testid="stSidebar"]) button[data-testid="stBaseButton-primary"], -section:not([data-testid="stSidebar"]) button[data-testid="stBaseButton-secondary"] { +section:not([data-testid="stSidebar"]) + button[data-testid="stBaseButton-primary"], +section:not([data-testid="stSidebar"]) + button[data-testid="stBaseButton-secondary"] { color: white !important; background: darkgreen !important; } -section:not([data-testid="stSidebar"]) button[data-testid="stBaseButton-primary"] p, -section:not([data-testid="stSidebar"]) button[data-testid="stBaseButton-secondary"] p { +section:not([data-testid="stSidebar"]) + button[data-testid="stBaseButton-primary"] + p, +section:not([data-testid="stSidebar"]) + button[data-testid="stBaseButton-secondary"] + p { color: white !important; } @@ -104,7 +115,9 @@ section:not([data-testid="stSidebar"]) div[role="radiogroup"] > label p { color: var(--radio-text) !important; } -section:not([data-testid="stSidebar"]) div[role="radiogroup"] > label[data-selected="true"] { +section:not([data-testid="stSidebar"]) + div[role="radiogroup"] + > label[data-selected="true"] { background-color: var(--radio-selected-bg) !important; color: var(--radio-selected-text) !important; } @@ -118,6 +131,10 @@ section[data-testid="stFileUploaderDropzone"] { padding: 4px; } +small { + display: none; +} + section:not([data-testid="stSidebar"]) div[data-testid="stSelectbox"] p, section:not([data-testid="stSidebar"]) div[data-testid="stMultiSelect"] p, section:not([data-testid="stSidebar"]) div[data-testid="stRadio"] p, @@ -208,7 +225,8 @@ table { margin-bottom: 1.5em; } -th, td { +th, +td { border: 1px solid var(--table-border) !important; padding: 8px; text-align: left; @@ -228,56 +246,11 @@ table[role="table"] th[scope="col"] { } /* ========================================== - 7. Composants spécifiques + 7. Composants spécifiques ========================================== */ -/* --- 7.1 File Uploader (traductions) --- */ -/* Hide original "Drag and drop file here" text */ -div[data-testid="stFileUploaderDropzoneInstructions"] span:nth-of-type(1) { - visibility: hidden; -} - -/* Insert French translation */ -div[data-testid="stFileUploaderDropzoneInstructions"] span:nth-of-type(1)::after { - content: "Glissez-déposez votre fichier ici"; - visibility: visible; - display: block; - font-size: inherit; - color: inherit; -} - -/* Hide original "Browse files" button text */ -div[data-testid="stFileUploaderDropzone"] button[data-testid="stBaseButton-secondary"] { - color: transparent !important; - position: relative; -} - -/* Insert French translation for button */ -div[data-testid="stFileUploaderDropzone"] button[data-testid="stBaseButton-secondary"]::after { - content: "Parcourir les fichiers"; - visibility: visible !important; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - display: block; - font-size: inherit; - color: inherit !important; -} - -/* Override Streamlit file uploader limit text */ -div[data-testid="stFileUploaderDropzoneInstructions"] small { - visibility: hidden; -} - -div[data-testid="stFileUploaderDropzoneInstructions"] small::after { - content: "Limite 100 Ko par fichier • JSON"; - visibility: visible; - display: block; - font-size: inherit; - color: inherit; - margin-top: 0.25em; -} +/* --- 7.1 File Uploader --- */ +/* File uploader styles intentionally left empty */ /* --- 7.2 Graphiques --- */ .stPlotlyChart text { @@ -312,7 +285,9 @@ details { border-color: var(--details-border) !important; } -section:not([data-testid="stSidebar"]) div:not[data-testid="stElementContainer"] p:not(#Authentification):not(#Theme) { +section:not([data-testid="stSidebar"]) + div:not[data-testid="stElementContainer"] + p:not(#Authentification):not(#Theme) { color: var(--paragraph-color) !important; } @@ -321,7 +296,8 @@ section:not([data-testid="stSidebar"]) hr { } /* --- 7.4 Conteneurs de commentaires et tickets --- */ -.conteneur_commentaire, .conteneur_ticket { +.conteneur_commentaire, +.conteneur_ticket { background: var(--background-color); padding: 1em; border-radius: 8px; @@ -329,12 +305,14 @@ section:not([data-testid="stSidebar"]) hr { border: 1px solid #ccc; } -.commentaire_auteur, .ticket_auteur { +.commentaire_auteur, +.ticket_auteur { color: var(--text-color) !important; margin: 0; } -.commentaire_contenu, .ticket_contenu { +.commentaire_contenu, +.ticket_contenu { color: var(--text-color) !important; margin: 0.5rem 0 0; } @@ -366,4 +344,4 @@ div.stElementContainer.element-container.st-key-nom_utilisateur { margin-left: auto; margin-right: auto; margin-top: 1rem; -} \ No newline at end of file +} diff --git a/components/connexion.py b/components/connexion.py index 660ecbd..cb6e392 100644 --- a/components/connexion.py +++ b/components/connexion.py @@ -2,6 +2,7 @@ import streamlit as st import requests import logging import os +from utils.translations import _ def initialiser_logger(): LOG_FILE_PATH = "/var/log/fabnum-auth.log" @@ -19,10 +20,11 @@ def initialiser_logger(): def connexion(): if "logged_in" not in st.session_state or not st.session_state.logged_in: - st.html(""" + auth_title = str(_("auth.title", "Authentification")) + st.html(f"""
-

Authentification

+

{auth_title}

""") GITEA_URL = "https://fabnum-git.peccini.fr/api/v1" @@ -40,9 +42,9 @@ def connexion(): with st.form("auth_form"): # Ajout d'un champ identifiant fictif pour activer l'autocomplétion navigateur # et permettre de stocker le token comme un mot de passe par le navigateur - identifiant = st.text_input("Identifiant_token", value="fabnum-connexion", key="nom_utilisateur") - token = st.text_input("Token d'accès personnel Gitea", type="password") - submitted = st.form_submit_button("Se connecter") + identifiant = st.text_input(str(_("auth.username", "Identifiant_token")), value="fabnum-connexion", key="nom_utilisateur") + token = st.text_input(str(_("auth.token", "Token d'accès personnel Gitea")), type="password") + submitted = st.form_submit_button(str(_("auth.login", "Se connecter"))) if submitted and token: erreur = True @@ -76,11 +78,11 @@ def connexion(): st.rerun() except requests.RequestException: - st.error("❌ Impossible de vérifier l'utilisateur auprès de Gitea.") + st.error(str(_("auth.gitea_error", "❌ Impossible de vérifier l'utilisateur auprès de Gitea."))) if erreur: logger.warning(f"Accès refusé pour tentative avec token depuis IP {ip}") - st.error("❌ Accès refusé.") + st.error(str(_("auth.error", "❌ Accès refusé."))) st.html("""
@@ -90,21 +92,22 @@ def connexion(): def bouton_deconnexion(): if st.session_state.get("logged_in", False): - st.html(""" + auth_title = str(_("auth.title", "Authentification")) + st.html(f"""
-

Authentification

+

{auth_title}

""") - st.sidebar.markdown(f"Connecté en tant que `{st.session_state.username}`") - if st.sidebar.button("Se déconnecter"): + st.sidebar.markdown(f"{str(_('auth.logged_as', 'Connecté en tant que'))} `{st.session_state.username}`") + if st.sidebar.button(str(_("auth.logout", "Se déconnecter"))): st.session_state.logged_in = False st.session_state.username = "" st.session_state.token = "" - st.success("Déconnecté avec succès.") + st.success(str(_("auth.success", "Déconnecté avec succès."))) st.rerun() st.html("""
- """) + """) \ No newline at end of file diff --git a/components/footer.py b/components/footer.py index b15af81..c512ac8 100644 --- a/components/footer.py +++ b/components/footer.py @@ -1,4 +1,5 @@ import streamlit as st +from utils.translations import _ def afficher_pied_de_page(): @@ -6,15 +7,15 @@ def afficher_pied_de_page():
""", unsafe_allow_html=True) - st.markdown(""" + st.markdown(f""" diff --git a/components/header.py b/components/header.py index 0c3c802..560b92b 100644 --- a/components/header.py +++ b/components/header.py @@ -1,18 +1,19 @@ import streamlit as st from config import ENV +from utils.translations import _ def afficher_entete(): - header = """ + header = f"""
-

FabNum - Chaîne de fabrication du numérique

+

{_("header.title", "FabNum - Chaîne de fabrication du numérique")}

""" if ENV == "dev": - header += "

🔧 Vous êtes dans l'environnement de développement.

" + header += f"

🔧 {_("app.dev_mode", "Vous êtes dans l'environnement de développement.")}

" else: - header += "

Parcours de l'écosystème et identification des vulnérabilités.

" + header += f"

{_("header.subtitle", "Parcours de l'écosystème et identification des vulnérabilités.")}

" header += """
diff --git a/components/sidebar.py b/components/sidebar.py index 5a45ba4..7f09212 100644 --- a/components/sidebar.py +++ b/components/sidebar.py @@ -1,26 +1,35 @@ import streamlit as st from components.connexion import connexion, bouton_deconnexion import streamlit.components.v1 as components +from utils.translations import _ def afficher_menu(): with st.sidebar: - st.markdown(""" -