Modification internationalisation

This commit is contained in:
Stéphan Peccini 2025-05-14 12:39:24 +02:00
parent 99ae3123e9
commit f4a28db6f7
20 changed files with 512 additions and 264 deletions

View File

@ -31,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(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 --"))
st.markdown(f"## {str(_('pages.analyse.selection_nodes'))}")
valeur_defaut = str(_("pages.analyse.select_level"))
niveau_choix = [valeur_defaut] + list(niveau_labels.values())
niveau_depart = st.selectbox(str(_("pages.analyse.start_level", "Niveau de départ")), niveau_choix, key="analyse_niveau_depart")
niveau_depart = st.selectbox(str(_("pages.analyse.start_level")), niveau_choix, key="analyse_niveau_depart")
if niveau_depart == valeur_defaut:
return None, None
@ -43,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(str(_("pages.analyse.end_level", "Niveau d'arrivée")), niveaux_arrivee_choix, key="analyse_niveau_arrivee")
analyse_niveau_arrivee = st.selectbox(str(_("pages.analyse.end_level")), niveaux_arrivee_choix, key="analyse_niveau_arrivee")
if analyse_niveau_arrivee == valeur_defaut:
return niveau_depart_int, None
@ -55,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(f"### {str(_('pages.analyse.select_minerals', 'Sélectionner un ou plusieurs minerais'))}")
st.markdown(f"### {str(_('pages.analyse.select_minerals'))}")
# Tous les nœuds de niveau 2 (minerai)
minerais_nodes = sorted([
n for n, d in G.nodes(data=True)
@ -63,7 +63,7 @@ def selectionner_minerais(G, niveau_depart, niveau_arrivee):
])
minerais_selection = st.multiselect(
str(_("pages.analyse.filter_by_minerals", "Filtrer par minerais (optionnel)")),
str(_("pages.analyse.filter_by_minerals")),
minerais_nodes,
key="analyse_minerais"
)
@ -74,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(f"## {str(_('pages.analyse.fine_selection', 'Sélection fine des items'))}")
st.markdown(f"## {str(_('pages.analyse.fine_selection'))}")
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(str(_("pages.analyse.filter_start_nodes", "Filtrer par noeuds de départ (optionnel)")),
noeuds_depart = st.multiselect(str(_("pages.analyse.filter_start_nodes")),
sorted(depart_nodes),
key="analyse_noeuds_depart")
noeuds_arrivee = st.multiselect(str(_("pages.analyse.filter_end_nodes", "Filtrer par noeuds d'arrivée (optionnel)")),
noeuds_arrivee = st.multiselect(str(_("pages.analyse.filter_end_nodes")),
sorted(arrivee_nodes),
key="analyse_noeuds_arrivee")
@ -95,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(f"## {str(_('pages.analyse.vulnerability_filters', 'Sélection des filtres pour identifier les vulnérabilités'))}")
st.markdown(f"## {str(_('pages.analyse.vulnerability_filters'))}")
filtrer_ics = st.checkbox(str(_("pages.analyse.filter_ics", "Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)")),
filtrer_ics = st.checkbox(str(_("pages.analyse.filter_ics")),
key="analyse_filtrer_ics")
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)")),
filtrer_ivc = st.checkbox(str(_("pages.analyse.filter_ivc")),
key="analyse_filtrer_ivc")
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)")),
filtrer_ihh = st.checkbox(str(_("pages.analyse.filter_ihh")),
key="analyse_filtrer_ihh")
ihh_type = "Pays"
if filtrer_ihh:
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"))],
ihh_type = st.radio(str(_("pages.analyse.apply_ihh_filter")),
[str(_("pages.analyse.countries")), str(_("pages.analyse.actors"))],
horizontal=True,
key="analyse_ihh_type")
filtrer_isg = st.checkbox(str(_("pages.analyse.filter_isg", "Filtrer les chemins contenant un pays instable (ISG ≥ 60)")),
filtrer_isg = st.checkbox(str(_("pages.analyse.filter_isg")),
key="analyse_filtrer_isg")
logique_filtrage = st.radio(str(_("pages.analyse.filter_logic", "Logique de filtrage")),
[str(_("pages.analyse.or", "OU")), str(_("pages.analyse.and", "ET"))],
logique_filtrage = st.radio(str(_("pages.analyse.filter_logic")),
[str(_("pages.analyse.or")), str(_("pages.analyse.and"))],
horizontal=True,
key="analyse_logique_filtrage")
@ -122,16 +122,9 @@ def configurer_filtres_vulnerabilite():
def interface_analyse(G_temp):
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(f"# {str(_('pages.analyse.title'))}")
with st.expander(str(_("pages.analyse.help")), expanded=False):
st.markdown("\n".join(_("pages.analyse.help_content")))
st.markdown("---")
try:
@ -155,7 +148,7 @@ def interface_analyse(G_temp):
# Lancement de l'analyse
st.markdown("---")
if st.button(str(_("pages.analyse.run_analysis", "Lancer l'analyse")), type="primary", key="analyse_lancer"):
if st.button(str(_("pages.analyse.run_analysis")), type="primary", key="analyse_lancer"):
afficher_sankey(
G_temp,
niveau_depart=niveau_depart,
@ -172,4 +165,4 @@ def interface_analyse(G_temp):
)
except Exception as e:
st.error(f"{str(_('errors.graph_preview_error', 'Erreur de prévisualisation du graphe :'))} {e}")
st.error(f"{str(_('errors.graph_preview_error'))} {e}")

View File

@ -187,16 +187,22 @@ def couleur_criticite(p):
def edge_info(G, u, v):
"""Génère l'info-bulle pour un lien"""
# Liste d'attributs à exclure des infobulles des liens
attributs_exclus = ["poids", "color", "fontcolor"]
data = G.get_edge_data(u, v)
if not data:
return f"{str(_('pages.analyse.sankey.relation', 'Relation'))} : {u}{v}"
return f"{str(_('pages.analyse.sankey.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"{str(_('pages.analyse.sankey.relation', 'Relation'))} : {u}{v}<br>" + "<br>".join(base)
base = [f"{k}: {v}" for k, v in data.items() if k not in attributs_exclus]
return f"{str(_('pages.analyse.sankey.relation'))} : {u}{v}<br>" + "<br>".join(base)
def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
"""Prépare les données pour le graphique Sankey"""
# Liste d'attributs à exclure des infobulles des nœuds
node_attributs_exclus = ["fillcolor", "niveau"]
df_liens = pd.DataFrame(list(liens_chemins), columns=["source", "target"])
df_liens = df_liens.groupby(["source", "target"]).size().reset_index(name="value")
@ -215,7 +221,7 @@ def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
noeuds_utilises.add(n)
df_liens["color"] = df_liens.apply(
lambda row: couleur_criticite(row["criticite"]) if row["criticite"] > 0 else "gray",
lambda row: couleur_criticite(row["criticite"]) if row["criticite"] > 0 else "white",
axis=1
)
@ -226,7 +232,7 @@ def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
customdata = []
for n in sorted_nodes:
info = [f"{k}: {v}" for k, v in G.nodes[n].items()]
info = [f"{k}: {v}" for k, v in G.nodes[n].items() if k not in node_attributs_exclus]
niveau = niveaux.get(n, 99)
# Ajout d'un ISG hérité si applicable
@ -268,12 +274,17 @@ def creer_graphique_sankey(G, niveaux, df_liens, sorted_nodes, customdata, link_
value=values,
color=df_liens["color"].tolist(),
customdata=link_customdata,
hovertemplate="%{customdata}<extra></extra>"
hovertemplate="%{customdata}<extra></extra>",
line=dict(
width=1, # Set fixed width to 3 pixels (or use 2 if preferred)
color="grey"
),
arrowlen=10
)
))
fig.update_layout(
title_text=str(_("pages.analyse.sankey.filtered_hierarchy", "Hiérarchie filtrée par niveaux et noeuds")),
title_text=str(_("pages.analyse.sankey.filtered_hierarchy")),
paper_bgcolor="white",
plot_bgcolor="white"
)
@ -303,7 +314,7 @@ def exporter_graphe_filtre(G, liens_chemins):
with open(dot_path, encoding="utf-8") as f:
st.download_button(
label=str(_("pages.analyse.sankey.download_dot", "Télécharger le fichier DOT filtré")),
label=str(_("pages.analyse.sankey.download_dot")),
data=f.read(),
file_name="graphe_filtré.dot",
mime="text/plain"
@ -325,7 +336,7 @@ def afficher_sankey(
chemins = extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais)
if not chemins:
st.warning(str(_("pages.analyse.sankey.no_paths", "Aucun chemin trouvé pour les critères spécifiés.")))
st.warning(str(_("pages.analyse.sankey.no_paths")))
return
# Étape 3 : Filtrage des chemins selon les critères de vulnérabilité
@ -335,12 +346,13 @@ def afficher_sankey(
)
if not liens_chemins:
st.warning(str(_("pages.analyse.sankey.no_matching_paths", "Aucun chemin ne correspond aux critères.")))
st.warning(str(_("pages.analyse.sankey.no_matching_paths")))
return
# Étape 4 : Préparation des données pour le graphique Sankey
df_liens, sorted_nodes, customdata, link_customdata, node_indices = preparer_donnees_sankey(
G, liens_chemins, niveaux, chemins_filtres if any([filtrer_ics, filtrer_ivc, filtrer_ihh, filtrer_isg]) else chemins
G, liens_chemins, niveaux,
chemins_filtres if any([filtrer_ics, filtrer_ivc, filtrer_ihh, filtrer_isg]) else chemins
)
# Étape 5 : Création et affichage du graphique Sankey

View File

@ -18,23 +18,9 @@ from .utils.fiche_utils import load_seuils, doit_regenerer_fiche
from .generer import generer_fiche
def interface_fiches():
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(f"# {str(_('pages.fiches.title'))}")
with st.expander(str(_("pages.fiches.help")), expanded=False):
st.markdown("\n".join(_("pages.fiches.help_content")))
st.markdown("---")
if "fiches_arbo" not in st.session_state:
@ -42,24 +28,24 @@ def interface_fiches():
arbo = st.session_state.get("fiches_arbo", {})
if not arbo:
st.warning(str(_("pages.fiches.no_files", "Aucune fiche disponible pour le moment.")))
st.warning(str(_("pages.fiches.no_files")))
return
dossiers = sorted(arbo.keys(), key=lambda x: x.lower())
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
str(_("pages.fiches.choose_category",)),
[str(_("pages.fiches.select_folder"))] + dossiers
)
if dossier_choisi and dossier_choisi != str(_("pages.fiches.select_folder", "-- Sélectionner un dossier --")):
if dossier_choisi and dossier_choisi != str(_("pages.fiches.select_folder")):
fiches = arbo.get(dossier_choisi, [])
noms_fiches = [f['nom'] for f in fiches]
fiche_choisie = st.selectbox(
str(_("pages.fiches.choose_file", "Choisissez une fiche")),
[str(_("pages.fiches.select_file", "-- Sélectionner une fiche --"))] + noms_fiches
str(_("pages.fiches.choose_file")),
[str(_("pages.fiches.select_file"))] + noms_fiches
)
if fiche_choisie and fiche_choisie != str(_("pages.fiches.select_file", "-- Sélectionner une fiche --")):
if fiche_choisie and fiche_choisie != str(_("pages.fiches.select_file")):
fiche_info = next((f for f in fiches if f["nom"] == fiche_choisie), None)
if fiche_info:
try:
@ -101,17 +87,17 @@ def interface_fiches():
if os.path.exists(pdf_path):
with open(pdf_path, "rb") as pdf_file:
st.download_button(
label=str(_("pages.fiches.download_pdf", "Télécharger cette fiche en PDF")),
label=str(_("pages.fiches.download_pdf")),
data=pdf_file,
file_name=pdf_name,
mime="application/pdf",
help=str(_("pages.fiches.download_pdf", "Télécharger cette fiche en PDF")),
help=str(_("pages.fiches.download_pdf")),
key="telecharger_fiche_pdf"
)
else:
st.warning(str(_("pages.fiches.pdf_unavailable", "Le fichier PDF de cette fiche n'est pas disponible.")))
st.warning(str(_("pages.fiches.pdf_unavailable")))
st.markdown(f"## {str(_('pages.fiches.ticket_management', 'Gestion des tickets pour cette fiche'))}")
st.markdown(f"## {str(_('pages.fiches.ticket_management'))}")
afficher_tickets_par_fiche(rechercher_tickets_gitea(fiche_choisie))
formulaire_creation_ticket_dynamique(fiche_choisie)

View File

@ -17,7 +17,7 @@ def gitea_request(method, url, **kwargs):
response.raise_for_status()
return response
except requests.RequestException as e:
st.error(f"{str(_('errors.gitea_error', 'Erreur Gitea'))} ({method.upper()}): {e}")
st.error(f"{str(_('errors.gitea_error'))} ({method.upper()}): {e}")
return None
@ -39,9 +39,9 @@ def charger_fiches_et_labels():
"item": item.strip()
}
except FileNotFoundError:
st.error(f"{str(_('errors.file_not_found', 'Le fichier'))} {chemin_csv} {str(_('errors.is_missing', 'est introuvable.'))}")
st.error(f"{str(_('errors.file_not_found'))} {chemin_csv} {str(_('errors.is_missing'))}")
except Exception as e:
st.error(f"{str(_('errors.file_loading', 'Erreur lors du chargement des fiches :'))} {str(e)}")
st.error(f"{str(_('errors.file_loading'))} {str(e)}")
return dictionnaire_fiches
@ -57,7 +57,7 @@ def rechercher_tickets_gitea(fiche_selectionnee):
try:
issues = reponse.json()
except Exception as e:
st.error(f"{str(_('errors.json_decode', 'Erreur de décodage JSON :'))} {e}")
st.error(f"{str(_('errors.json_decode'))} {e}")
return []
correspondances = charger_fiches_et_labels()
@ -87,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"{str(_('errors.label_parsing', 'Erreur de parsing des labels :'))} {e}")
st.error(f"{str(_('errors.label_parsing'))} {e}")
return {}
@ -114,6 +114,6 @@ def creer_ticket_gitea(titre, corps, labels):
issue_url = reponse.json().get("html_url", "")
if issue_url:
st.success(f"{str(_('pages.fiches.tickets.created_success', 'Ticket créé !'))} [Voir le ticket]({issue_url})")
st.success(f"{str(_('pages.fiches.tickets.created_success'))} [Voir le ticket]({issue_url})")
else:
st.success(str(_('pages.fiches.tickets.created', 'Ticket créé avec succès.')))
st.success(str(_('pages.fiches.tickets.created')))

View File

@ -14,7 +14,7 @@ def parser_modele_ticket(contenu_modele):
sections = {}
lignes = contenu_modele.splitlines()
titre_courant, contenu = None, []
for ligne in lignes:
if ligne.startswith("## "):
if titre_courant:
@ -22,10 +22,10 @@ def parser_modele_ticket(contenu_modele):
titre_courant, contenu = ligne[3:].strip(), []
elif titre_courant:
contenu.append(ligne)
if titre_courant:
sections[titre_courant] = "\n".join(contenu).strip()
return sections
@ -34,47 +34,47 @@ def generer_labels(fiche_selectionnee):
labels, selected_ops = [], []
correspondances = charger_fiches_et_labels()
cible = correspondances.get(fiche_selectionnee)
if cible:
if len(cible["operations"]) == 1:
labels.append(cible["operations"][0])
elif len(cible["operations"]) > 1:
selected_ops = st.multiselect(str(_("pages.fiches.tickets.contribution_type", "Labels opération à associer")),
cible["operations"],
selected_ops = st.multiselect(str(_("pages.fiches.tickets.contribution_type")),
cible["operations"],
default=cible["operations"])
return labels, selected_ops, cible
def creer_champs_formulaire(sections, fiche_selectionnee):
"""Crée les champs du formulaire basés sur les sections."""
reponses = {}
for section, aide in sections.items():
if "Type de contribution" in section:
options = sorted(set(re.findall(r"- \[.\] (.+)", aide)))
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
if str(_("pages.fiches.tickets.other")) not in options:
options.append(str(_("pages.fiches.tickets.other")))
choix = st.radio(str(_("pages.fiches.tickets.contribution_type")), options)
reponses[section] = st.text_input(str(_("pages.fiches.tickets.specify")), "") if choix == str(_("pages.fiches.tickets.other")) 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(str(_("pages.fiches.tickets.concerned_card", "Fiche concernée")), value=url_fiche, disabled=True)
st.text_input(str(_("pages.fiches.tickets.concerned_card")), value=url_fiche, disabled=True)
elif "Sujet de la proposition" in section:
reponses[section] = st.text_input(section, help=aide)
else:
reponses[section] = st.text_area(section, help=aide)
return reponses
def afficher_controles_formulaire():
"""Affiche les boutons de contrôle du formulaire."""
col1, col2 = st.columns(2)
if col1.button(str(_("pages.fiches.tickets.preview", "Prévisualiser le ticket"))):
if col1.button(str(_("pages.fiches.tickets.preview"))):
st.session_state.previsualiser = True
if col2.button(str(_("pages.fiches.tickets.cancel", "Annuler"))):
if col2.button(str(_("pages.fiches.tickets.cancel"))):
st.session_state.previsualiser = False
st.rerun()
@ -83,8 +83,8 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible):
"""Gère la prévisualisation et la soumission du ticket."""
if not st.session_state.get("previsualiser", False):
return
st.subheader(str(_("pages.fiches.tickets.preview_title", "Prévisualisation du ticket")))
st.subheader(str(_("pages.fiches.tickets.preview_title")))
for section, texte in reponses.items():
st.markdown(f"#### {section}")
st.code(texte, language="markdown")
@ -92,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"**{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)}`")
st.markdown(f"**{str(_('pages.fiches.tickets.summary'))} :**\n- **{str(_('pages.fiches.tickets.title'))}** : `{titre_ticket}`\n- **{str(_('pages.fiches.tickets.labels'))}** : `{', '.join(final_labels)}`")
if st.button(str(_("pages.fiches.tickets.confirm", "Confirmer la création du ticket"))):
if st.button(str(_("pages.fiches.tickets.confirm"))):
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:
@ -104,23 +104,23 @@ 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(str(_("pages.fiches.tickets.created", "Ticket créé et formulaire vidé.")))
st.success(str(_("pages.fiches.tickets.created")))
def formulaire_creation_ticket_dynamique(fiche_selectionnee):
"""Fonction principale pour le formulaire de création de ticket."""
with st.expander(str(_("pages.fiches.tickets.create_new", "Créer un nouveau ticket lié à cette fiche")), expanded=False):
with st.expander(str(_("pages.fiches.tickets.create_new")), expanded=False):
# Chargement et vérification du modèle
contenu_modele = charger_modele_ticket()
if not contenu_modele:
st.error(str(_("pages.fiches.tickets.model_load_error", "Impossible de charger le modèle de ticket.")))
st.error(str(_("pages.fiches.tickets.model_load_error")))
return
# Traitement du modèle et génération du formulaire
sections = parser_modele_ticket(contenu_modele)
labels, selected_ops, cible = generer_labels(fiche_selectionnee)
reponses = creer_champs_formulaire(sections, fiche_selectionnee)
# Gestion des contrôles et de la prévisualisation
afficher_controles_formulaire()
gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible)
@ -136,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"{str(_('pages.fiches.tickets.model_error', 'Erreur chargement modèle :'))} {e}")
st.error(f"{str(_('pages.fiches.tickets.model_error'))} {e}")
return ""

View File

@ -11,22 +11,22 @@ 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",
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"))]:
for statut in ["Backlog",
str(_("pages.fiches.tickets.status.awaiting")),
str(_("pages.fiches.tickets.status.in_progress")),
str(_("pages.fiches.tickets.status.completed")),
str(_("pages.fiches.tickets.status.rejected"))]:
if statut in labels:
return statut
return str(_("pages.fiches.tickets.status.others", "Autres"))
return str(_("pages.fiches.tickets.status.others"))
def afficher_tickets_par_fiche(tickets):
if not tickets:
st.info(str(_("pages.fiches.tickets.no_linked_tickets", "Aucun ticket lié à cette fiche.")))
st.info(str(_("pages.fiches.tickets.no_linked_tickets")))
return
st.markdown(str(_("pages.fiches.tickets.associated_tickets", "**Tickets associés à cette fiche**")))
st.markdown(str(_("pages.fiches.tickets.associated_tickets")))
tickets_groupes = defaultdict(list)
for ticket in tickets:
statut = extraire_statut_par_label(ticket)
@ -34,14 +34,14 @@ def afficher_tickets_par_fiche(tickets):
nb_backlogs = len(tickets_groupes["Backlog"])
if nb_backlogs:
st.info(f"{nb_backlogs} {str(_('pages.fiches.tickets.moderation_notice', 'ticket(s) en attente de modération ne sont pas affichés.'))}")
st.info(f"{nb_backlogs} {str(_('pages.fiches.tickets.moderation_notice'))}")
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"))
str(_("pages.fiches.tickets.status.awaiting")),
str(_("pages.fiches.tickets.status.in_progress")),
str(_("pages.fiches.tickets.status.completed")),
str(_("pages.fiches.tickets.status.rejected")),
str(_("pages.fiches.tickets.status.others"))
]
for statut in ordre_statuts:
if tickets_groupes[statut]:
@ -61,14 +61,14 @@ def recuperer_commentaires_ticket(issue_index):
response.raise_for_status()
return response.json()
except Exception as e:
st.error(f"{str(_('pages.fiches.tickets.comment_error', 'Erreur lors de la récupération des commentaires :'))} {e}")
st.error(f"{str(_('pages.fiches.tickets.comment_error'))} {e}")
return []
def afficher_carte_ticket(ticket):
titre = ticket.get("title", str(_("pages.fiches.tickets.no_title", "Sans titre")))
titre = ticket.get("title", str(_("pages.fiches.tickets.no_title")))
url = ticket.get("html_url", "")
user = ticket.get("user", {}).get("login", str(_("pages.fiches.tickets.unknown", "inconnu")))
user = ticket.get("user", {}).get("login", str(_("pages.fiches.tickets.unknown")))
created = ticket.get("created_at", "")
updated = ticket.get("updated_at", "")
body = ticket.get("body", "")
@ -86,12 +86,12 @@ def afficher_carte_ticket(ticket):
return "?"
date_created_str = format_date(created)
maj_info = f"({str(_('pages.fiches.tickets.updated', 'MAJ'))} {format_date(updated)})" if updated and updated != created else ""
maj_info = f"({str(_('pages.fiches.tickets.updated'))} {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', str(_("pages.fiches.tickets.unknown", "inconnu"))))
auteur = html.escape(commentaire.get('user', {}).get('login', str(_("pages.fiches.tickets.unknown"))))
contenu = html.escape(commentaire.get('body', ''))
date = format_date(commentaire.get('created_at', ''))
commentaires_html += f"""
@ -105,12 +105,12 @@ def afficher_carte_ticket(ticket):
st.markdown(f"""
<div class=\"conteneur_ticket\">
<h4><a href='{url}' target='_blank'>{titre}</a></h4>
<p>{str(_("pages.fiches.tickets.opened_by", "Ouvert par"))} <strong>{html.escape(user)}</strong> {str(_("pages.fiches.tickets.on_date", "le"))} {date_created_str} {maj_info}</p>
<p>{str(_("pages.fiches.tickets.subject_label", "Sujet"))} : <strong>{html.escape(sujet)}</strong></p>
<p>Labels : {''.join(labels) if labels else str(_("pages.fiches.tickets.no_labels", "aucun"))}</p>
<p>{str(_("pages.fiches.tickets.opened_by"))} <strong>{html.escape(user)}</strong> {str(_("pages.fiches.tickets.on_date"))} {date_created_str} {maj_info}</p>
<p>{str(_("pages.fiches.tickets.subject_label"))} : <strong>{html.escape(sujet)}</strong></p>
<p>Labels : {''.join(labels) if labels else str(_("pages.fiches.tickets.no_labels"))}</p>
</div>
""", unsafe_allow_html=True)
st.markdown(body, unsafe_allow_html=False)
st.markdown("---")
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)
st.markdown(str(_("pages.fiches.tickets.comments")))
st.markdown(commentaires_html or str(_("pages.fiches.tickets.no_comments")), unsafe_allow_html=True)

View File

@ -2,22 +2,22 @@ import streamlit as st
from utils.translations import _
def ajouter_produit(G):
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")
st.markdown(f"## {str(_('pages.personnalisation.add_new_product'))}")
new_prod = st.text_input(str(_("pages.personnalisation.new_product_name")), 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(str(_("pages.personnalisation.assembly_operation", "Opération d'assemblage (optionnelle)")), [str(_("pages.personnalisation.none", "-- Aucune --"))] + ops_dispo, index=0)
sel_new_op = st.selectbox(str(_("pages.personnalisation.assembly_operation")), [str(_("pages.personnalisation.none"))] + ops_dispo, index=0)
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
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"))):
sel_comps = st.multiselect(str(_("pages.personnalisation.components_to_link")), options=niveau1)
if st.button(str(_("pages.personnalisation.create_product"))):
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
if sel_new_op != str(_("pages.personnalisation.none", "-- Aucune --")):
if sel_new_op != str(_("pages.personnalisation.none")):
G.add_edge(new_prod, sel_new_op)
for comp in sel_comps:
G.add_edge(new_prod, comp)
st.success(f"{new_prod} {str(_('pages.personnalisation.added', 'ajouté'))}")
st.success(f"{new_prod} {str(_('pages.personnalisation.added'))}")
return G

View File

@ -1,24 +1,25 @@
import streamlit as st
import json
from utils.translations import get_translation as _
def importer_exporter_graph(G):
st.markdown("## Sauvegarder ou restaurer la configuration")
if st.button("Exporter configuration"):
st.markdown(f"## {_('pages.personnalisation.save_restore_config')}")
if st.button(str(_("pages.personnalisation.export_config"))):
nodes = [n for n, d in G.nodes(data=True) if d.get("personnalisation") == "oui"]
edges = [(u, v) for u, v in G.edges() if u in nodes]
conf = {"nodes": nodes, "edges": edges}
json_str = json.dumps(conf, ensure_ascii=False)
st.download_button(
label="Télécharger (JSON)",
label=str(_("pages.personnalisation.download_json")),
data=json_str,
file_name="config_personnalisation.json",
mime="application/json"
)
uploaded = st.file_uploader("Importer une configuration JSON (max 100 Ko)", type=["json"])
uploaded = st.file_uploader(str(_("pages.personnalisation.import_config")), type=["json"])
if uploaded:
if uploaded.size > 100 * 1024:
st.error("Fichier trop volumineux (max 100 Ko).")
st.error(_("pages.personnalisation.file_too_large"))
else:
try:
conf = json.loads(uploaded.read().decode("utf-8"))
@ -26,17 +27,17 @@ def importer_exporter_graph(G):
all_edges = conf.get("edges", [])
if not all_nodes:
st.warning("Aucun produit trouvé dans le fichier.")
st.warning(_("pages.personnalisation.no_products_found"))
else:
st.markdown("### Sélection des produits à restaurer")
st.markdown(f"### {_('pages.personnalisation.select_products_to_restore')}")
sel_nodes = st.multiselect(
"Produits à restaurer",
str(_("pages.personnalisation.products_to_restore")),
options=all_nodes,
default=all_nodes,
key="restaurer_selection"
)
if st.button("Restaurer les éléments sélectionnés", type="primary"):
if st.button(str(_("pages.personnalisation.restore_selected")), type="primary"):
for node in sel_nodes:
if not G.has_node(node):
G.add_node(node, niveau="0", personnalisation="oui", label=node)
@ -45,8 +46,8 @@ def importer_exporter_graph(G):
if u in sel_nodes and v in sel_nodes + list(G.nodes()) and not G.has_edge(u, v):
G.add_edge(u, v)
st.success("Configuration partielle restaurée avec succès.")
st.success(_("pages.personnalisation.config_restored"))
except Exception as e:
st.error(f"Erreur d'import : {e}")
st.error(f"{_('errors.import_error')} {e}")
return G

View File

@ -7,18 +7,9 @@ from .modification import modifier_produit
from .import_export import importer_exporter_graph
def interface_personnalisation(G):
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(f"# {str(_('pages.personnalisation.title'))}")
with st.expander(str(_("pages.personnalisation.help")), expanded=False):
st.markdown("\n".join(_("pages.personnalisation.help_content")))
st.markdown("---")
G = ajouter_produit(G)

View File

@ -8,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} {str(_('pages.personnalisation.deleted', 'supprimé'))}")
st.success(f"{prod} {str(_('pages.personnalisation.deleted'))}")
st.session_state.pop("prod_sel", None)
return G
@ -51,37 +51,37 @@ def mettre_a_jour_composants(G, prod, linked, nouveaux):
return G
def modifier_produit(G):
st.markdown(f"## {str(_('pages.personnalisation.modify_product', 'Modifier un produit final ajouté'))}")
st.markdown(f"## {str(_('pages.personnalisation.modify_product'))}")
# Sélection du produit à modifier
produits0 = get_produits_personnalises(G)
sel_display = st.multiselect(str(_("pages.personnalisation.products_to_modify", "Produits à modifier")), options=produits0)
sel_display = st.multiselect(str(_("pages.personnalisation.products_to_modify")), options=produits0)
if not sel_display:
return G
# Obtention du produit sélectionné
prod = sel_display[0]
# Suppression du produit si demandé
if st.button(f"{str(_('pages.personnalisation.delete', 'Supprimer'))} {prod}"):
if st.button(f"{str(_('pages.personnalisation.delete'))} {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(str(_("pages.personnalisation.linked_assembly_operation", "Opération d'assemblage liée")), [str(_("pages.personnalisation.none", "-- Aucune --"))] + ops_dispo, index=default_idx)
sel_op = st.selectbox(str(_("pages.personnalisation.linked_assembly_operation")), [str(_("pages.personnalisation.none"))] + ops_dispo, index=default_idx)
# Gestion des composants
niveau1 = get_composants_niveau1(G)
linked = get_composants_lies(G, prod)
nouveaux = st.multiselect(f"{str(_('pages.personnalisation.components_linked_to', 'Composants liés à'))} {prod}", options=niveau1, default=linked)
nouveaux = st.multiselect(f"{str(_('pages.personnalisation.components_linked_to'))} {prod}", options=niveau1, default=linked)
# Mise à jour des liens si demandé
if st.button(f"{str(_('pages.personnalisation.update', 'Mettre à jour'))} {prod}"):
if st.button(f"{str(_('pages.personnalisation.update'))} {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} {str(_('pages.personnalisation.updated', 'mis à jour'))}")
st.success(f"{prod} {str(_('pages.personnalisation.updated'))}")
return G

View File

@ -9,18 +9,18 @@ from utils.translations import _
def afficher_graphique_altair(df):
# 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"))
"Assemblage": str(_("pages.visualisations.categories.assembly")),
"Fabrication": str(_("pages.visualisations.categories.manufacturing")),
"Traitement": str(_("pages.visualisations.categories.processing")),
"Extraction": str(_("pages.visualisations.categories.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
@ -53,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=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 (%)"))),
x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries"))),
y=alt.Y('ihh_acteurs:Q', title=str(_("pages.visualisations.axis_titles.ihh_actors"))),
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']))
)
@ -83,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=str(_("pages.visualisations.chart_titles.concentration_criticality", "Concentration et criticité {0}")).format(cat_traduit)
title=str(_("pages.visualisations.chart_titles.concentration_criticality")).format(cat_traduit)
).interactive()
st.altair_chart(chart, use_container_width=True)
@ -91,7 +91,7 @@ def afficher_graphique_altair(df):
def creer_graphes(donnees):
if not donnees:
st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à afficher.")))
st.warning(str(_("pages.visualisations.no_data")))
return
try:
@ -122,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=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 (%)"))),
x=alt.X('ihh_extraction:Q', title=str(_("pages.visualisations.axis_titles.ihh_extraction"))),
y=alt.Y('ihh_reserves:Q', title=str(_("pages.visualisations.axis_titles.ihh_reserves"))),
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']
@ -153,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=str(_("pages.visualisations.chart_titles.concentration_resources", "Concentration des ressources critiques vs vulnérabilité IVC"))
title=str(_("pages.visualisations.chart_titles.concentration_resources"))
).interactive()
st.altair_chart(chart, use_container_width=True)
except Exception as e:
st.error(f"{str(_('errors.graph_creation_error', 'Erreur lors de la création du graphique :'))} {e}")
st.error(f"{str(_('errors.graph_creation_error'))} {e}")
def lancer_visualisation_ihh_criticite(graph):
@ -173,11 +173,11 @@ def lancer_visualisation_ihh_criticite(graph):
df = recuperer_donnees(graph, noeuds)
if df.empty:
st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à visualiser.")))
st.warning(str(_("pages.visualisations.no_data")))
else:
afficher_graphique_altair(df)
except Exception as e:
st.error(f"{str(_('errors.ihh_criticality_error', 'Erreur dans la visualisation IHH vs Criticité :'))} {e}")
st.error(f"{str(_('errors.ihh_criticality_error'))} {e}")
def lancer_visualisation_ihh_ivc(graph):
@ -192,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"{str(_('errors.ihh_ivc_error', 'Erreur dans la visualisation IHH vs IVC :'))} {e}")
st.error(f"{str(_('errors.ihh_ivc_error'))} {e}")

View File

@ -8,39 +8,28 @@ from .graphes import (
def interface_visualisations(G_temp, G_temp_ivc):
st.markdown(f"# {str(_('pages.visualisations.title', 'Visualisations'))}")
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(f"# {str(_('pages.visualisations.title'))}")
with st.expander(str(_("pages.visualisations.help")), expanded=False):
st.markdown("\n".join(_("pages.visualisations.help_content")))
st.markdown("---")
st.markdown(f"""## {str(_("pages.visualisations.ihh_criticality", "Indice de Herfindahl-Hirschmann - IHH vs Criticité"))}
st.markdown(f"""## {str(_("pages.visualisations.ihh_criticality"))}
{str(_("pages.visualisations.ihh_criticality_desc", "La taille des points donne l'indication de la criticité de substituabilité du minerai."))}
{str(_("pages.visualisations.ihh_criticality_desc"))}
""")
if st.button(str(_("buttons.run", "Lancer")), key="btn_ihh_criticite"):
if st.button(str(_("buttons.run")), key="btn_ihh_criticite"):
try:
lancer_visualisation_ihh_criticite(G_temp)
except Exception as e:
st.error(f"{str(_('errors.ihh_criticality_error', 'Erreur dans la visualisation IHH vs Criticité :'))} {e}")
st.error(f"{str(_('errors.ihh_criticality_error'))} {e}")
st.markdown(f"""## {str(_("pages.visualisations.ihh_ivc", "Indice de Herfindahl-Hirschmann - IHH vs IVC"))}
st.markdown(f"""## {str(_("pages.visualisations.ihh_ivc"))}
{str(_("pages.visualisations.ihh_ivc_desc", "La taille des points donne l'indication de la criticité concurrentielle du minerai."))}
{str(_("pages.visualisations.ihh_ivc_desc"))}
""")
if st.button(str(_("buttons.run", "Lancer")), key="btn_ihh_ivc"):
if st.button(str(_("buttons.run")), key="btn_ihh_ivc"):
try:
lancer_visualisation_ihh_ivc(G_temp_ivc)
except Exception as e:
st.error(f"{str(_('errors.ihh_ivc_error', 'Erreur dans la visualisation IHH vs IVC :'))} {e}")
st.error(f"{str(_('errors.ihh_ivc_error'))} {e}")

276
assets/locales/en copy.json Normal file
View File

@ -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": "Visualizations",
"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"
}
}
}

View File

@ -20,7 +20,7 @@ def initialiser_logger():
def connexion():
if "logged_in" not in st.session_state or not st.session_state.logged_in:
auth_title = str(_("auth.title", "Authentification"))
auth_title = str(_("auth.title"))
st.html(f"""
<section role="region" aria-label="region-authentification">
<div role="region" aria-labelledby="Authentification">
@ -42,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(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")))
identifiant = st.text_input(str(_("auth.username")), value="fabnum-connexion", key="nom_utilisateur")
token = st.text_input(str(_("auth.token")), type="password")
submitted = st.form_submit_button(str(_("auth.login")))
if submitted and token:
erreur = True
@ -78,11 +78,11 @@ def connexion():
st.rerun()
except requests.RequestException:
st.error(str(_("auth.gitea_error", "❌ Impossible de vérifier l'utilisateur auprès de Gitea.")))
st.error(str(_("auth.gitea_error")))
if erreur:
logger.warning(f"Accès refusé pour tentative avec token depuis IP {ip}")
st.error(str(_("auth.error", "❌ Accès refusé.")))
st.error(str(_("auth.error")))
st.html("""
</div>
@ -92,22 +92,22 @@ def connexion():
def bouton_deconnexion():
if st.session_state.get("logged_in", False):
auth_title = str(_("auth.title", "Authentification"))
auth_title = str(_("auth.title"))
st.html(f"""
<section role="region" aria-label="region-authentification">
<div role="region" aria-labelledby="Authentification">
<p id="Authentification" class="decorative-heading">{auth_title}</p>
""")
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.sidebar.markdown(f"{str(_('auth.logged_as'))} `{st.session_state.username}`")
if st.sidebar.button(str(_("auth.logout"))):
st.session_state.logged_in = False
st.session_state.username = ""
st.session_state.token = ""
st.success(str(_("auth.success", "Déconnecté avec succès.")))
st.success(str(_("auth.success")))
st.rerun()
st.html("""
</div>
</section>
""")
""")

View File

@ -11,11 +11,11 @@ def afficher_pied_de_page():
<div role='contentinfo' aria-labelledby='footer-appli' class='wide-footer'>
<div class='info-footer'>
<p id='footer-appli' class='info-footer'>
{_("footer.copyright", "Fabnum © 2025")} <a href='mailto:stephan-pro@peccini.fr'>{_("footer.contact", "Contact")}</a> {_("footer.license", "Licence")} <a href='https://creativecommons.org/licenses/by-nc-nd/4.0/deed.fr' target='_blank'>{_("footer.license_text", "CC BY-NC-ND")}</a>
{_("footer.copyright")} <a href='mailto:stephan-pro@peccini.fr'>{_("footer.contact")}</a> {_("footer.license")} <a href='https://creativecommons.org/licenses/by-nc-nd/4.0/deed.fr' target='_blank'>{_("footer.license_text")}</a>
</p>
<p class='footer-note'>
{_("footer.eco_note", "🌱 Calculs eqCO₂ via")} <a href='https://www.thegreenwebfoundation.org/' target='_blank'>{_("footer.eco_provider", "The Green Web Foundation")}</a><br>
{_("footer.powered_by", "🚀 Propulsé par")} <a href='https://streamlit.io/' target='_blank'>{_("footer.powered_by_name", "Streamlit")}</a>
{_("footer.eco_note")} <a href='https://www.thegreenwebfoundation.org/' target='_blank'>{_("footer.eco_provider")}</a><br>
{_("footer.powered_by")} <a href='https://streamlit.io/' target='_blank'>{_("footer.powered_by_name")}</a>
</p>
</div>
</div>

View File

@ -7,13 +7,13 @@ def afficher_entete():
header = f"""
<header role="banner" aria-labelledby="entete-header">
<div class='wide-header'>
<p id='entete-header' class='titre-header'>{_("header.title", "FabNum - Chaîne de fabrication du numérique")}</p>
<p id='entete-header' class='titre-header'>{_("header.title")}</p>
"""
if ENV == "dev":
header += f"<p>🔧 {_("app.dev_mode", "Vous êtes dans l'environnement de développement.")}</p>"
header += f"<p>🔧 {_("app.dev_mode")}</p>"
else:
header += f"<p>{_("header.subtitle", "Parcours de l'écosystème et identification des vulnérabilités.")}</p>"
header += f"<p>{_("header.subtitle")}</p>"
header += """
</div>

View File

@ -7,22 +7,22 @@ from utils.translations import _
def afficher_menu():
with st.sidebar:
st.markdown(f"""
<nav role="navigation" aria-label="{str(_('sidebar.menu', 'Menu principal'))}">
<div role="region" aria-label="{str(_('sidebar.navigation', 'Navigation principale'))}" class="onglets-accessibles">
<nav role="navigation" aria-label="{str(_('sidebar.menu'))}">
<div role="region" aria-label="{str(_('sidebar.navigation'))}" class="onglets-accessibles">
""", unsafe_allow_html=True)
# Définir la variable instructions_text une seule fois en haut de la fonction
instructions_text = str(_("navigation.instructions", "Instructions"))
instructions_text = str(_("navigation.instructions"))
if "onglet" not in st.session_state:
st.session_state.onglet = instructions_text
onglet_choisi = None
onglets = [
str(_("navigation.instructions", "Instructions")),
str(_("navigation.personnalisation", "Personnalisation")),
str(_("navigation.analyse", "Analyse")),
str(_("navigation.visualisations", "Visualisations")),
str(_("navigation.fiches", "Fiches"))
str(_("navigation.instructions")),
str(_("navigation.personnalisation")),
str(_("navigation.analyse")),
str(_("navigation.visualisations")),
str(_("navigation.fiches"))
]
for nom in onglets:
@ -45,9 +45,9 @@ def afficher_menu():
#
if st.session_state.onglet == instructions_text:
if "theme_mode" not in st.session_state:
st.session_state.theme_mode = str(_("sidebar.theme_light", "Clair"))
st.session_state.theme_mode = str(_("sidebar.theme_light"))
theme_title = str(_("sidebar.theme", "Thème"))
theme_title = str(_("sidebar.theme"))
st.markdown(f"""
<section role="region" aria-label="region-theme">
<div role="region" aria-labelledby="Theme">
@ -55,11 +55,11 @@ def afficher_menu():
""", unsafe_allow_html=True)
theme_options = [
str(_("sidebar.theme_light", "Clair")),
str(_("sidebar.theme_dark", "Sombre"))
str(_("sidebar.theme_light")),
str(_("sidebar.theme_dark"))
]
theme = st.radio(
str(_("sidebar.theme", "Thème")),
str(_("sidebar.theme")),
theme_options,
index=theme_options.index(st.session_state.theme_mode),
horizontal=True,
@ -71,14 +71,14 @@ def afficher_menu():
</div>
</nav>""", unsafe_allow_html=True)
else :
theme_title = str(_("sidebar.theme", "Thème"))
theme_title = str(_("sidebar.theme"))
st.markdown(f"""
<section role="region" aria-label="region-theme">
<div role="region" aria-labelledby="Theme">
<p id="Theme" class="decorative-heading">{theme_title}</p>
""", unsafe_allow_html=True)
st.info(str(_("sidebar.theme_instructions_only", "Le changement de thème ne peut se faire que depuis l'onglet Instructions.")))
st.info(str(_("sidebar.theme_instructions_only")))
st.markdown("""
<hr />
@ -101,8 +101,8 @@ def afficher_menu():
def afficher_impact(total_bytes):
impact_label = str(_("sidebar.impact", "Impact environnemental"))
loading_text = str(_("sidebar.loading", "Chargement en cours…"))
impact_label = str(_("sidebar.impact"))
loading_text = str(_("sidebar.loading"))
with st.sidebar:
components.html(f"""

View File

@ -154,11 +154,11 @@ ouvrir_page()
dot_file_path = None
# Obtenir les noms traduits des onglets
instructions_tab = _("navigation.instructions", "Instructions")
fiches_tab = _("navigation.fiches", "Fiches")
personnalisation_tab = _("navigation.personnalisation", "Personnalisation")
analyse_tab = _("navigation.analyse", "Analyse")
visualisations_tab = _("navigation.visualisations", "Visualisations")
instructions_tab = _("navigation.instructions")
fiches_tab = _("navigation.fiches")
personnalisation_tab = _("navigation.personnalisation")
analyse_tab = _("navigation.analyse")
visualisations_tab = _("navigation.visualisations")
if st.session_state.onglet == instructions_tab:
markdown_content = charger_instructions_depuis_gitea(INSTRUCTIONS)

View File

@ -31,7 +31,7 @@ def load_translations(lang="fr"):
logger.error(f"Erreur lors du chargement des traductions: {e}")
return {}
def get_translation(key, default=None):
def get_translation(key):
"""
Récupère une traduction par sa clé.
Les clés peuvent être hiérarchiques, séparées par des points.
@ -51,7 +51,7 @@ def get_translation(key, default=None):
# Si aucune traduction n'est chargée, retourner la valeur par défaut
if not st.session_state.get("translations"):
return default if default is not None else key
return f"⊗⤇ {key} ⤆⊗"
# Parcourir la hiérarchie des clés
keys = key.split(".")
@ -59,7 +59,7 @@ def get_translation(key, default=None):
for k in keys:
if not isinstance(current, dict) or k not in current:
return default if default is not None else key
return f"⊗⤇ {key} ⤆⊗"
current = current[k]
return current

View File

@ -8,10 +8,10 @@ from utils.translations import _
def afficher_graphique_altair(df):
ordre_personnalise = [
str(_("pages.visualisations.categories.assembly", "Assemblage")),
str(_("pages.visualisations.categories.manufacturing", "Fabrication")),
str(_("pages.visualisations.categories.processing", "Traitement")),
str(_("pages.visualisations.categories.extraction", "Extraction"))
str(_("pages.visualisations.categories.assembly")),
str(_("pages.visualisations.categories.manufacturing")),
str(_("pages.visualisations.categories.processing")),
str(_("pages.visualisations.categories.extraction"))
]
categories = [cat for cat in ordre_personnalise if cat in df['categorie'].unique()]
for cat in categories:
@ -42,8 +42,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=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 (%)"))),
x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries"))),
y=alt.Y('ihh_acteurs:Q', title=str(_("pages.visualisations.axis_titles.ihh_actors"))),
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']))
)
@ -70,7 +70,7 @@ def afficher_graphique_altair(df):
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
width=500,
height=400,
title=str(_("pages.visualisations.chart_titles.concentration_criticality", "Concentration et criticité {0}")).format(str(cat))
title=str(_("pages.visualisations.chart_titles.concentration_criticality")).format(str(cat))
).interactive()
st.altair_chart(chart, use_container_width=True)
@ -78,7 +78,7 @@ def afficher_graphique_altair(df):
def creer_graphes(donnees):
if not donnees:
st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à afficher.")))
st.warning(str(_("pages.visualisations.no_data")))
return
try:
@ -109,8 +109,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=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 (%)"))),
x=alt.X('ihh_extraction:Q', title=str(_("pages.visualisations.axis_titles.ihh_extraction"))),
y=alt.Y('ihh_reserves:Q', title=str(_("pages.visualisations.axis_titles.ihh_reserves"))),
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']
@ -138,13 +138,13 @@ def creer_graphes(donnees):
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
width=600,
height=500,
title=str(_("pages.visualisations.chart_titles.concentration_resources", "Concentration des ressources critiques vs vulnérabilité IVC"))
title=str(_("pages.visualisations.chart_titles.concentration_resources"))
).interactive()
st.altair_chart(chart, use_container_width=True)
except Exception as e:
st.error(f"{str(_('errors.graph_creation_error', 'Erreur lors de la création du graphique :'))} {e}")
st.error(f"{str(_('errors.graph_creation_error'))} {e}")
def lancer_visualisation_ihh_criticite(graph):
@ -158,11 +158,11 @@ def lancer_visualisation_ihh_criticite(graph):
df = recuperer_donnees(graph, noeuds)
if df.empty:
st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à visualiser.")))
st.warning(str(_("pages.visualisations.no_data")))
else:
afficher_graphique_altair(df)
except Exception as e:
st.error(f"{str(_('errors.ihh_criticality_error', 'Erreur dans la visualisation IHH vs Criticité :'))} {e}")
st.error(f"{str(_('errors.ihh_criticality_error'))} {e}")
def lancer_visualisation_ihh_ivc(graph):
@ -177,4 +177,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"{str(_('errors.ihh_ivc_error', 'Erreur dans la visualisation IHH vs IVC :'))} {e}")
st.error(f"{str(_('errors.ihh_ivc_error'))} {e}")