Préparation à l'internationalisation
This commit is contained in:
parent
96682783b6
commit
059e94b0f3
@ -1,4 +1,5 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
from .sankey import afficher_sankey
|
from .sankey import afficher_sankey
|
||||||
|
|
||||||
@ -30,11 +31,11 @@ def preparer_graphe(G):
|
|||||||
|
|
||||||
def selectionner_niveaux():
|
def selectionner_niveaux():
|
||||||
"""Interface pour sélectionner les niveaux de départ et d'arrivée."""
|
"""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")
|
st.markdown(f"## {str(_('pages.analyse.selection_nodes', 'Sélection des nœuds de départ et d\'arrivée'))}")
|
||||||
valeur_defaut = "-- Sélectionner un niveau --"
|
valeur_defaut = str(_("pages.analyse.select_level", "-- Sélectionner un niveau --"))
|
||||||
niveau_choix = [valeur_defaut] + list(niveau_labels.values())
|
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:
|
if niveau_depart == valeur_defaut:
|
||||||
return None, None
|
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_possibles = [v for k, v in niveau_labels.items() if k > niveau_depart_int]
|
||||||
niveaux_arrivee_choix = [valeur_defaut] + niveaux_arrivee_possibles
|
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:
|
if analyse_niveau_arrivee == valeur_defaut:
|
||||||
return niveau_depart_int, None
|
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."""
|
"""Interface pour sélectionner les minerais si nécessaire."""
|
||||||
minerais_selection = None
|
minerais_selection = None
|
||||||
if niveau_depart < 2 < niveau_arrivee:
|
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)
|
# Tous les nœuds de niveau 2 (minerai)
|
||||||
minerais_nodes = sorted([
|
minerais_nodes = sorted([
|
||||||
n for n, d in G.nodes(data=True)
|
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(
|
minerais_selection = st.multiselect(
|
||||||
"Filtrer par minerais (optionnel)",
|
str(_("pages.analyse.filter_by_minerals", "Filtrer par minerais (optionnel)")),
|
||||||
minerais_nodes,
|
minerais_nodes,
|
||||||
key="analyse_minerais"
|
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):
|
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."""
|
"""Interface pour sélectionner les nœuds spécifiques de départ et d'arrivée."""
|
||||||
st.markdown("---")
|
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]
|
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]
|
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),
|
sorted(depart_nodes),
|
||||||
key="analyse_noeuds_depart")
|
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),
|
sorted(arrivee_nodes),
|
||||||
key="analyse_noeuds_arrivee")
|
key="analyse_noeuds_arrivee")
|
||||||
|
|
||||||
@ -94,26 +95,26 @@ def selectionner_noeuds(G, niveaux_temp, niveau_depart, niveau_arrivee):
|
|||||||
def configurer_filtres_vulnerabilite():
|
def configurer_filtres_vulnerabilite():
|
||||||
"""Interface pour configurer les filtres de vulnérabilité."""
|
"""Interface pour configurer les filtres de vulnérabilité."""
|
||||||
st.markdown("---")
|
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")
|
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")
|
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")
|
key="analyse_filtrer_ihh")
|
||||||
|
|
||||||
ihh_type = "Pays"
|
ihh_type = "Pays"
|
||||||
if filtrer_ihh:
|
if filtrer_ihh:
|
||||||
ihh_type = st.radio("Appliquer le filtre IHH sur :",
|
ihh_type = st.radio(str(_("pages.analyse.apply_ihh_filter", "Appliquer le filtre IHH sur :")),
|
||||||
["Pays", "Acteurs"],
|
[str(_("pages.analyse.countries", "Pays")), str(_("pages.analyse.actors", "Acteurs"))],
|
||||||
horizontal=True,
|
horizontal=True,
|
||||||
key="analyse_ihh_type")
|
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")
|
key="analyse_filtrer_isg")
|
||||||
logique_filtrage = st.radio("Logique de filtrage",
|
logique_filtrage = st.radio(str(_("pages.analyse.filter_logic", "Logique de filtrage")),
|
||||||
["OU", "ET"],
|
[str(_("pages.analyse.or", "OU")), str(_("pages.analyse.and", "ET"))],
|
||||||
horizontal=True,
|
horizontal=True,
|
||||||
key="analyse_logique_filtrage")
|
key="analyse_logique_filtrage")
|
||||||
|
|
||||||
@ -121,16 +122,16 @@ def configurer_filtres_vulnerabilite():
|
|||||||
|
|
||||||
|
|
||||||
def interface_analyse(G_temp):
|
def interface_analyse(G_temp):
|
||||||
st.markdown("# Analyse du graphe")
|
st.markdown(f"# {str(_('pages.analyse.title', 'Analyse du graphe'))}")
|
||||||
with st.expander("Comment utiliser cet onglet ?", expanded=False):
|
with st.expander(str(_("pages.analyse.help", "Comment utiliser cet onglet ?")), expanded=False):
|
||||||
st.markdown("""
|
st.markdown("\n".join(_("pages.analyse.help_content", [
|
||||||
1. Sélectionnez le niveau de départ (produit final, composant ou minerai)
|
"1. Sélectionnez le niveau de départ (produit final, composant ou minerai)",
|
||||||
2. Choisissez le niveau d'arrivée souhaité
|
"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)
|
"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
|
"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
|
"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
|
"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("---")
|
st.markdown("---")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -154,7 +155,7 @@ def interface_analyse(G_temp):
|
|||||||
|
|
||||||
# Lancement de l'analyse
|
# Lancement de l'analyse
|
||||||
st.markdown("---")
|
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(
|
afficher_sankey(
|
||||||
G_temp,
|
G_temp,
|
||||||
niveau_depart=niveau_depart,
|
niveau_depart=niveau_depart,
|
||||||
@ -171,4 +172,4 @@ def interface_analyse(G_temp):
|
|||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
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}")
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import plotly.graph_objects as go
|
|||||||
import networkx as nx
|
import networkx as nx
|
||||||
import logging
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
from utils.graph_utils import (
|
from utils.graph_utils import (
|
||||||
extraire_chemins_depuis,
|
extraire_chemins_depuis,
|
||||||
@ -188,11 +189,11 @@ def edge_info(G, u, v):
|
|||||||
"""Génère l'info-bulle pour un lien"""
|
"""Génère l'info-bulle pour un lien"""
|
||||||
data = G.get_edge_data(u, v)
|
data = G.get_edge_data(u, v)
|
||||||
if not data:
|
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):
|
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
|
||||||
data = data[0]
|
data = data[0]
|
||||||
base = [f"{k}: {v}" for k, v in data.items()]
|
base = [f"{k}: {v}" for k, v in data.items()]
|
||||||
return f"Relation : {u} → {v}<br>" + "<br>".join(base)
|
return f"{str(_('pages.analyse.sankey.relation', 'Relation'))} : {u} → {v}<br>" + "<br>".join(base)
|
||||||
|
|
||||||
def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
|
def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
|
||||||
"""Prépare les données pour le graphique Sankey"""
|
"""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(
|
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",
|
paper_bgcolor="white",
|
||||||
plot_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:
|
with open(dot_path, encoding="utf-8") as f:
|
||||||
st.download_button(
|
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(),
|
data=f.read(),
|
||||||
file_name="graphe_filtré.dot",
|
file_name="graphe_filtré.dot",
|
||||||
mime="text/plain"
|
mime="text/plain"
|
||||||
@ -324,7 +325,7 @@ def afficher_sankey(
|
|||||||
chemins = extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais)
|
chemins = extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais)
|
||||||
|
|
||||||
if not chemins:
|
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
|
return
|
||||||
|
|
||||||
# Étape 3 : Filtrage des chemins selon les critères de vulnérabilité
|
# Étape 3 : Filtrage des chemins selon les critères de vulnérabilité
|
||||||
@ -334,7 +335,7 @@ def afficher_sankey(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not liens_chemins:
|
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
|
return
|
||||||
|
|
||||||
# Étape 4 : Préparation des données pour le graphique Sankey
|
# Étape 4 : Préparation des données pour le graphique Sankey
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import streamlit as st
|
|||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
from .utils.tickets.display import afficher_tickets_par_fiche
|
from .utils.tickets.display import afficher_tickets_par_fiche
|
||||||
from .utils.tickets.creation import formulaire_creation_ticket_dynamique
|
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
|
from .generer import generer_fiche
|
||||||
|
|
||||||
def interface_fiches():
|
def interface_fiches():
|
||||||
st.markdown("# Découverte des fiches")
|
st.markdown(f"# {str(_('pages.fiches.title', 'Découverte des fiches'))}")
|
||||||
with st.expander("Comment utiliser cet onglet ?", expanded=False):
|
with st.expander(str(_("pages.fiches.help", "Comment utiliser cet onglet ?")), expanded=False):
|
||||||
st.markdown("""
|
st.markdown("\n".join([
|
||||||
1. Parcourez la liste des fiches disponibles par catégorie
|
" " + line for line in _("pages.fiches.help_content", [
|
||||||
2. Sélectionnez une fiche pour afficher son contenu complet
|
"1. Parcourez la liste des fiches disponibles par catégorie",
|
||||||
3. Consultez les données détaillées, graphiques et analyses supplémentaires
|
"2. Sélectionnez une fiche pour afficher son contenu complet",
|
||||||
4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées
|
"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
|
"Les catégories sont les suivantes :",
|
||||||
* Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition
|
"* Assemblage : opération d'assemblage des produits finaux à partir des composants",
|
||||||
* Criticités : indices utilisés pour identifier et évaluer les vulnérabilités
|
"* Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition",
|
||||||
* Fabrication : opération de fabrication des composants à partir de minerais
|
"* Criticités : indices utilisés pour identifier et évaluer les vulnérabilités",
|
||||||
* Minerai : description et opérations d'extraction et de traitement des minerais
|
"* Fabrication : opération de fabrication des composants à partir de minerais",
|
||||||
""")
|
"* Minerai : description et opérations d'extraction et de traitement des minerais"
|
||||||
|
])
|
||||||
|
]))
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
if "fiches_arbo" not in st.session_state:
|
if "fiches_arbo" not in st.session_state:
|
||||||
@ -39,18 +42,24 @@ def interface_fiches():
|
|||||||
|
|
||||||
arbo = st.session_state.get("fiches_arbo", {})
|
arbo = st.session_state.get("fiches_arbo", {})
|
||||||
if not 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
|
return
|
||||||
|
|
||||||
dossiers = sorted(arbo.keys(), key=lambda x: x.lower())
|
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, [])
|
fiches = arbo.get(dossier_choisi, [])
|
||||||
noms_fiches = [f['nom'] for f in fiches]
|
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)
|
fiche_info = next((f for f in fiches if f["nom"] == fiche_choisie), None)
|
||||||
if fiche_info:
|
if fiche_info:
|
||||||
try:
|
try:
|
||||||
@ -92,19 +101,19 @@ def interface_fiches():
|
|||||||
if os.path.exists(pdf_path):
|
if os.path.exists(pdf_path):
|
||||||
with open(pdf_path, "rb") as pdf_file:
|
with open(pdf_path, "rb") as pdf_file:
|
||||||
st.download_button(
|
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,
|
data=pdf_file,
|
||||||
file_name=pdf_name,
|
file_name=pdf_name,
|
||||||
mime="application/pdf",
|
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"
|
key="telecharger_fiche_pdf"
|
||||||
)
|
)
|
||||||
else:
|
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))
|
afficher_tickets_par_fiche(rechercher_tickets_gitea(fiche_choisie))
|
||||||
formulaire_creation_ticket_dynamique(fiche_choisie)
|
formulaire_creation_ticket_dynamique(fiche_choisie)
|
||||||
|
|
||||||
except Exception as e:
|
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}")
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
from utils.translations import _
|
||||||
from config import GITEA_URL, GITEA_TOKEN, ORGANISATION, DEPOT_FICHES, ENV
|
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()
|
response.raise_for_status()
|
||||||
return response
|
return response
|
||||||
except requests.RequestException as e:
|
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
|
return None
|
||||||
|
|
||||||
|
|
||||||
@ -38,9 +39,9 @@ def charger_fiches_et_labels():
|
|||||||
"item": item.strip()
|
"item": item.strip()
|
||||||
}
|
}
|
||||||
except FileNotFoundError:
|
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:
|
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
|
return dictionnaire_fiches
|
||||||
|
|
||||||
@ -56,7 +57,7 @@ def rechercher_tickets_gitea(fiche_selectionnee):
|
|||||||
try:
|
try:
|
||||||
issues = reponse.json()
|
issues = reponse.json()
|
||||||
except Exception as e:
|
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 []
|
return []
|
||||||
|
|
||||||
correspondances = charger_fiches_et_labels()
|
correspondances = charger_fiches_et_labels()
|
||||||
@ -86,7 +87,7 @@ def get_labels_existants():
|
|||||||
try:
|
try:
|
||||||
return {label['name']: label['id'] for label in reponse.json()}
|
return {label['name']: label['id'] for label in reponse.json()}
|
||||||
except Exception as e:
|
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 {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
@ -113,6 +114,6 @@ def creer_ticket_gitea(titre, corps, labels):
|
|||||||
|
|
||||||
issue_url = reponse.json().get("html_url", "")
|
issue_url = reponse.json().get("html_url", "")
|
||||||
if issue_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:
|
else:
|
||||||
st.success("Ticket créé avec succès.")
|
st.success(str(_('pages.fiches.tickets.created', 'Ticket créé avec succès.')))
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
import re
|
import re
|
||||||
import base64
|
import base64
|
||||||
import streamlit as st
|
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 .core import charger_fiches_et_labels, construire_corps_ticket_markdown, creer_ticket_gitea, get_labels_existants, nettoyer_labels
|
||||||
from config import ENV
|
from config import ENV
|
||||||
import requests
|
import requests
|
||||||
@ -38,7 +39,7 @@ def generer_labels(fiche_selectionnee):
|
|||||||
if len(cible["operations"]) == 1:
|
if len(cible["operations"]) == 1:
|
||||||
labels.append(cible["operations"][0])
|
labels.append(cible["operations"][0])
|
||||||
elif len(cible["operations"]) > 1:
|
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"],
|
cible["operations"],
|
||||||
default=cible["operations"])
|
default=cible["operations"])
|
||||||
|
|
||||||
@ -52,14 +53,14 @@ def creer_champs_formulaire(sections, fiche_selectionnee):
|
|||||||
for section, aide in sections.items():
|
for section, aide in sections.items():
|
||||||
if "Type de contribution" in section:
|
if "Type de contribution" in section:
|
||||||
options = sorted(set(re.findall(r"- \[.\] (.+)", aide)))
|
options = sorted(set(re.findall(r"- \[.\] (.+)", aide)))
|
||||||
if "Autre" not in options:
|
if str(_("pages.fiches.tickets.other", "Autre")) not in options:
|
||||||
options.append("Autre")
|
options.append(str(_("pages.fiches.tickets.other", "Autre")))
|
||||||
choix = st.radio("Type de contribution", options)
|
choix = st.radio(str(_("pages.fiches.tickets.contribution_type", "Type de contribution")), options)
|
||||||
reponses[section] = st.text_input("Précisez", "") if choix == "Autre" else choix
|
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:
|
elif "Fiche concernée" in section:
|
||||||
url_fiche = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/{fiche_selectionnee.replace(' ', '%20')}"
|
url_fiche = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/{fiche_selectionnee.replace(' ', '%20')}"
|
||||||
reponses[section] = url_fiche
|
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:
|
elif "Sujet de la proposition" in section:
|
||||||
reponses[section] = st.text_input(section, help=aide)
|
reponses[section] = st.text_input(section, help=aide)
|
||||||
else:
|
else:
|
||||||
@ -71,9 +72,9 @@ def creer_champs_formulaire(sections, fiche_selectionnee):
|
|||||||
def afficher_controles_formulaire():
|
def afficher_controles_formulaire():
|
||||||
"""Affiche les boutons de contrôle du formulaire."""
|
"""Affiche les boutons de contrôle du formulaire."""
|
||||||
col1, col2 = st.columns(2)
|
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
|
st.session_state.previsualiser = True
|
||||||
if col2.button("Annuler"):
|
if col2.button(str(_("pages.fiches.tickets.cancel", "Annuler"))):
|
||||||
st.session_state.previsualiser = False
|
st.session_state.previsualiser = False
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
@ -83,7 +84,7 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible):
|
|||||||
if not st.session_state.get("previsualiser", False):
|
if not st.session_state.get("previsualiser", False):
|
||||||
return
|
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():
|
for section, texte in reponses.items():
|
||||||
st.markdown(f"#### {section}")
|
st.markdown(f"#### {section}")
|
||||||
st.code(texte, language="markdown")
|
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"
|
titre_ticket = reponses.get("Sujet de la proposition", "").strip() or "Ticket FabNum"
|
||||||
final_labels = nettoyer_labels(labels + selected_ops + ([cible["item"]] if cible else []))
|
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_existants = get_labels_existants()
|
||||||
labels_ids = [labels_existants[l] for l in final_labels if l in labels_existants]
|
labels_ids = [labels_existants[l] for l in final_labels if l in labels_existants]
|
||||||
if "Backlog" 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)
|
creer_ticket_gitea(titre_ticket, corps, labels_ids)
|
||||||
|
|
||||||
st.session_state.previsualiser = False
|
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):
|
def formulaire_creation_ticket_dynamique(fiche_selectionnee):
|
||||||
"""Fonction principale pour le formulaire de création de ticket."""
|
"""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
|
# Chargement et vérification du modèle
|
||||||
contenu_modele = charger_modele_ticket()
|
contenu_modele = charger_modele_ticket()
|
||||||
if not contenu_modele:
|
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
|
return
|
||||||
|
|
||||||
# Traitement du modèle et génération du formulaire
|
# Traitement du modèle et génération du formulaire
|
||||||
@ -135,5 +136,5 @@ def charger_modele_ticket():
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return base64.b64decode(r.json().get("content", "")).decode("utf-8")
|
return base64.b64decode(r.json().get("content", "")).decode("utf-8")
|
||||||
except Exception as e:
|
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 ""
|
return ""
|
||||||
|
|||||||
@ -5,23 +5,28 @@ import html
|
|||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
from utils.translations import _
|
||||||
from .core import rechercher_tickets_gitea
|
from .core import rechercher_tickets_gitea
|
||||||
|
|
||||||
|
|
||||||
def extraire_statut_par_label(ticket):
|
def extraire_statut_par_label(ticket):
|
||||||
labels = [label.get('name', '') for label in ticket.get('labels', [])]
|
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:
|
if statut in labels:
|
||||||
return statut
|
return statut
|
||||||
return "Autres"
|
return str(_("pages.fiches.tickets.status.others", "Autres"))
|
||||||
|
|
||||||
|
|
||||||
def afficher_tickets_par_fiche(tickets):
|
def afficher_tickets_par_fiche(tickets):
|
||||||
if not 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
|
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)
|
tickets_groupes = defaultdict(list)
|
||||||
for ticket in tickets:
|
for ticket in tickets:
|
||||||
statut = extraire_statut_par_label(ticket)
|
statut = extraire_statut_par_label(ticket)
|
||||||
@ -29,9 +34,15 @@ def afficher_tickets_par_fiche(tickets):
|
|||||||
|
|
||||||
nb_backlogs = len(tickets_groupes["Backlog"])
|
nb_backlogs = len(tickets_groupes["Backlog"])
|
||||||
if nb_backlogs:
|
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:
|
for statut in ordre_statuts:
|
||||||
if tickets_groupes[statut]:
|
if tickets_groupes[statut]:
|
||||||
with st.expander(f"{statut} ({len(tickets_groupes[statut])})", expanded=(statut == "En cours")):
|
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()
|
response.raise_for_status()
|
||||||
return response.json()
|
return response.json()
|
||||||
except Exception as e:
|
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 []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def afficher_carte_ticket(ticket):
|
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", "")
|
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", "")
|
created = ticket.get("created_at", "")
|
||||||
updated = ticket.get("updated_at", "")
|
updated = ticket.get("updated_at", "")
|
||||||
body = ticket.get("body", "")
|
body = ticket.get("body", "")
|
||||||
@ -75,12 +86,12 @@ def afficher_carte_ticket(ticket):
|
|||||||
return "?"
|
return "?"
|
||||||
|
|
||||||
date_created_str = format_date(created)
|
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 = recuperer_commentaires_ticket(ticket.get("number"))
|
||||||
commentaires_html = ""
|
commentaires_html = ""
|
||||||
for commentaire in commentaires:
|
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', ''))
|
contenu = html.escape(commentaire.get('body', ''))
|
||||||
date = format_date(commentaire.get('created_at', ''))
|
date = format_date(commentaire.get('created_at', ''))
|
||||||
commentaires_html += f"""
|
commentaires_html += f"""
|
||||||
@ -94,12 +105,12 @@ def afficher_carte_ticket(ticket):
|
|||||||
st.markdown(f"""
|
st.markdown(f"""
|
||||||
<div class=\"conteneur_ticket\">
|
<div class=\"conteneur_ticket\">
|
||||||
<h4><a href='{url}' target='_blank'>{titre}</a></h4>
|
<h4><a href='{url}' target='_blank'>{titre}</a></h4>
|
||||||
<p>Ouvert par <strong>{html.escape(user)}</strong> le {date_created_str} {maj_info}</p>
|
<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>Sujet : <strong>{html.escape(sujet)}</strong></p>
|
<p>{str(_("pages.fiches.tickets.subject_label", "Sujet"))} : <strong>{html.escape(sujet)}</strong></p>
|
||||||
<p>Labels : {' • '.join(labels) if labels else 'aucun'}</p>
|
<p>Labels : {' • '.join(labels) if labels else str(_("pages.fiches.tickets.no_labels", "aucun"))}</p>
|
||||||
</div>
|
</div>
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
st.markdown(body, unsafe_allow_html=False)
|
st.markdown(body, unsafe_allow_html=False)
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.markdown("**Commentaire(s) :**")
|
st.markdown(str(_("pages.fiches.tickets.comments", "**Commentaire(s) :**")))
|
||||||
st.markdown(commentaires_html or "Aucun commentaire.", unsafe_allow_html=True)
|
st.markdown(commentaires_html or str(_("pages.fiches.tickets.no_comments", "Aucun commentaire.")), unsafe_allow_html=True)
|
||||||
|
|||||||
@ -1,22 +1,23 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
def ajouter_produit(G):
|
def ajouter_produit(G):
|
||||||
st.markdown("## Ajouter un nouveau produit final")
|
st.markdown(f"## {str(_('pages.personnalisation.add_new_product', 'Ajouter un nouveau produit final'))}")
|
||||||
new_prod = st.text_input("Nom du nouveau produit (unique)", key="new_prod")
|
new_prod = st.text_input(str(_("pages.personnalisation.new_product_name", "Nom du nouveau produit (unique)")), key="new_prod")
|
||||||
if new_prod:
|
if new_prod:
|
||||||
ops_dispo = sorted([
|
ops_dispo = sorted([
|
||||||
n for n, d in G.nodes(data=True)
|
n for n, d in G.nodes(data=True)
|
||||||
if d.get("niveau") == "10"
|
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))
|
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"])
|
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
|
||||||
sel_comps = st.multiselect("Composants à lier", options=niveau1)
|
sel_comps = st.multiselect(str(_("pages.personnalisation.components_to_link", "Composants à lier")), options=niveau1)
|
||||||
if st.button("Créer le produit"):
|
if st.button(str(_("pages.personnalisation.create_product", "Créer le produit"))):
|
||||||
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
|
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)
|
G.add_edge(new_prod, sel_new_op)
|
||||||
for comp in sel_comps:
|
for comp in sel_comps:
|
||||||
G.add_edge(new_prod, comp)
|
G.add_edge(new_prod, comp)
|
||||||
st.success(f"{new_prod} ajouté.")
|
st.success(f"{new_prod} {str(_('pages.personnalisation.added', 'ajouté'))}")
|
||||||
return G
|
return G
|
||||||
|
|||||||
@ -1,21 +1,24 @@
|
|||||||
# interface.py – app/personnalisation
|
# interface.py – app/personnalisation
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
from utils.translations import _
|
||||||
from .ajout import ajouter_produit
|
from .ajout import ajouter_produit
|
||||||
from .modification import modifier_produit
|
from .modification import modifier_produit
|
||||||
from .import_export import importer_exporter_graph
|
from .import_export import importer_exporter_graph
|
||||||
|
|
||||||
def interface_personnalisation(G):
|
def interface_personnalisation(G):
|
||||||
st.markdown("# Personnalisation des produits finaux")
|
st.markdown(f"# {str(_('pages.personnalisation.title', 'Personnalisation des produits finaux'))}")
|
||||||
with st.expander("Comment utiliser cet onglet ?", expanded=False):
|
with st.expander(str(_("pages.personnalisation.help", "Comment utiliser cet onglet ?")), expanded=False):
|
||||||
st.markdown("""
|
st.markdown("\n".join([
|
||||||
1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit
|
" " + line for line in _("pages.personnalisation.help_content", [
|
||||||
2. Donnez un nom à votre produit
|
"1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit",
|
||||||
3. Sélectionnez une opération d'assemblage appropriée (si pertinent)
|
"2. Donnez un nom à votre produit",
|
||||||
4. Choisissez les composants qui constituent votre produit dans la liste proposée
|
"3. Sélectionnez une opération d'assemblage appropriée (si pertinent)",
|
||||||
5. Sauvegardez votre configuration pour une réutilisation future
|
"4. Choisissez les composants qui constituent votre produit dans la liste proposée",
|
||||||
6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés
|
"5. Sauvegardez votre configuration pour une réutilisation future",
|
||||||
""")
|
"6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés"
|
||||||
|
])
|
||||||
|
]))
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
G = ajouter_produit(G)
|
G = ajouter_produit(G)
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
def get_produits_personnalises(G):
|
def get_produits_personnalises(G):
|
||||||
"""Récupère la liste des produits personnalisés du niveau 0."""
|
"""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):
|
def supprimer_produit(G, prod):
|
||||||
"""Supprime un produit du graphe."""
|
"""Supprime un produit du graphe."""
|
||||||
G.remove_node(prod)
|
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)
|
st.session_state.pop("prod_sel", None)
|
||||||
return G
|
return G
|
||||||
|
|
||||||
@ -33,10 +34,11 @@ def get_composants_lies(G, prod):
|
|||||||
|
|
||||||
def mettre_a_jour_operations(G, prod, curr_ops, sel_op):
|
def mettre_a_jour_operations(G, prod, curr_ops, sel_op):
|
||||||
"""Met à jour les opérations liées au produit."""
|
"""Met à jour les opérations liées au produit."""
|
||||||
|
none_option = str(_("pages.personnalisation.none", "-- Aucune --"))
|
||||||
for op in curr_ops:
|
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)
|
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)
|
G.add_edge(prod, sel_op)
|
||||||
return G
|
return G
|
||||||
|
|
||||||
@ -49,11 +51,11 @@ def mettre_a_jour_composants(G, prod, linked, nouveaux):
|
|||||||
return G
|
return G
|
||||||
|
|
||||||
def modifier_produit(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
|
# Sélection du produit à modifier
|
||||||
produits0 = get_produits_personnalises(G)
|
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:
|
if not sel_display:
|
||||||
return G
|
return G
|
||||||
@ -62,24 +64,24 @@ def modifier_produit(G):
|
|||||||
prod = sel_display[0]
|
prod = sel_display[0]
|
||||||
|
|
||||||
# Suppression du produit si demandé
|
# 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)
|
return supprimer_produit(G, prod)
|
||||||
|
|
||||||
# Gestion des opérations d'assemblage
|
# Gestion des opérations d'assemblage
|
||||||
ops_dispo = get_operations_disponibles(G)
|
ops_dispo = get_operations_disponibles(G)
|
||||||
curr_ops = get_operations_actuelles(G, prod)
|
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
|
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
|
# Gestion des composants
|
||||||
niveau1 = get_composants_niveau1(G)
|
niveau1 = get_composants_niveau1(G)
|
||||||
linked = get_composants_lies(G, prod)
|
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é
|
# 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_operations(G, prod, curr_ops, sel_op)
|
||||||
G = mettre_a_jour_composants(G, prod, linked, nouveaux)
|
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
|
return G
|
||||||
|
|||||||
@ -3,14 +3,31 @@ import altair as alt
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
|
|
||||||
def afficher_graphique_altair(df):
|
def afficher_graphique_altair(df):
|
||||||
ordre_personnalise = ['Assemblage', 'Fabrication', 'Traitement', 'Extraction']
|
# Définir les catégories originales (en français) et leur ordre
|
||||||
categories = [cat for cat in ordre_personnalise if cat in df['categorie'].unique()]
|
categories_fr = ["Assemblage", "Fabrication", "Traitement", "Extraction"]
|
||||||
for cat in categories:
|
|
||||||
st.markdown(f"### {cat}")
|
# Créer un dictionnaire de mappage entre les catégories originales et leurs traductions
|
||||||
df_cat = df[df['categorie'] == cat].copy()
|
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)))
|
coord_pairs = list(zip(df_cat['ihh_pays'].round(1), df_cat['ihh_acteurs'].round(1)))
|
||||||
counts = Counter(coord_pairs)
|
counts = Counter(coord_pairs)
|
||||||
@ -36,8 +53,8 @@ def afficher_graphique_altair(df):
|
|||||||
df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5
|
df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5
|
||||||
|
|
||||||
base = alt.Chart(df_cat).encode(
|
base = alt.Chart(df_cat).encode(
|
||||||
x=alt.X('ihh_pays:Q', title='IHH Pays (%)'),
|
x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries", "IHH Pays (%)"))),
|
||||||
y=alt.Y('ihh_acteurs:Q', title='IHH Acteurs (%)'),
|
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),
|
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']))
|
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(
|
chart = (points + lines + labels + hline_15 + hline_25 + hline_100 + vline_15 + vline_25 + vline_100).properties(
|
||||||
width=500,
|
width=500,
|
||||||
height=400,
|
height=400,
|
||||||
title=f"Concentration et criticité – {cat}"
|
title=str(_("pages.visualisations.chart_titles.concentration_criticality", "Concentration et criticité – {0}")).format(cat_traduit)
|
||||||
).interactive()
|
).interactive()
|
||||||
|
|
||||||
st.altair_chart(chart, use_container_width=True)
|
st.altair_chart(chart, use_container_width=True)
|
||||||
@ -74,7 +91,7 @@ def afficher_graphique_altair(df):
|
|||||||
|
|
||||||
def creer_graphes(donnees):
|
def creer_graphes(donnees):
|
||||||
if not donnees:
|
if not donnees:
|
||||||
st.warning("Aucune donnée à afficher.")
|
st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à afficher.")))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -105,8 +122,8 @@ def creer_graphes(donnees):
|
|||||||
df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5
|
df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5
|
||||||
|
|
||||||
base = alt.Chart(df).encode(
|
base = alt.Chart(df).encode(
|
||||||
x=alt.X('ihh_extraction:Q', title='IHH Extraction (%)'),
|
x=alt.X('ihh_extraction:Q', title=str(_("pages.visualisations.axis_titles.ihh_extraction", "IHH Extraction (%)"))),
|
||||||
y=alt.Y('ihh_reserves:Q', title='IHH Réserves (%)'),
|
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),
|
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'])),
|
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']
|
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(
|
chart = (points + lines + labels + hline_15 + hline_25 + hline_100 + vline_15 + vline_25 + vline_100).properties(
|
||||||
width=600,
|
width=600,
|
||||||
height=500,
|
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()
|
).interactive()
|
||||||
|
|
||||||
st.altair_chart(chart, use_container_width=True)
|
st.altair_chart(chart, use_container_width=True)
|
||||||
|
|
||||||
except Exception as e:
|
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):
|
def lancer_visualisation_ihh_criticite(graph):
|
||||||
@ -156,11 +173,11 @@ def lancer_visualisation_ihh_criticite(graph):
|
|||||||
|
|
||||||
df = recuperer_donnees(graph, noeuds)
|
df = recuperer_donnees(graph, noeuds)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
st.warning("Aucune donnée à visualiser.")
|
st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à visualiser.")))
|
||||||
else:
|
else:
|
||||||
afficher_graphique_altair(df)
|
afficher_graphique_altair(df)
|
||||||
except Exception as e:
|
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):
|
def lancer_visualisation_ihh_ivc(graph):
|
||||||
@ -175,4 +192,4 @@ def lancer_visualisation_ihh_ivc(graph):
|
|||||||
data = recuperer_donnees_2(graph, noeuds_niveau_2)
|
data = recuperer_donnees_2(graph, noeuds_niveau_2)
|
||||||
creer_graphes(data)
|
creer_graphes(data)
|
||||||
except Exception as e:
|
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}")
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
from .graphes import (
|
from .graphes import (
|
||||||
lancer_visualisation_ihh_criticite,
|
lancer_visualisation_ihh_criticite,
|
||||||
@ -7,39 +8,39 @@ from .graphes import (
|
|||||||
|
|
||||||
|
|
||||||
def interface_visualisations(G_temp, G_temp_ivc):
|
def interface_visualisations(G_temp, G_temp_ivc):
|
||||||
st.markdown("# Analyse du graphe")
|
st.markdown(f"# {str(_('pages.visualisations.title', 'Analyse du graphe'))}")
|
||||||
with st.expander("Comment utiliser cet onglet ?", expanded=False):
|
with st.expander(str(_("pages.visualisations.help", "Comment utiliser cet onglet ?")), expanded=False):
|
||||||
st.markdown("""
|
st.markdown("\n".join(_("pages.visualisations.help_content", [
|
||||||
1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH)
|
"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)
|
"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
|
"3. Zoomer dans les graphes pour mieux découvrir les informations",
|
||||||
|
"",
|
||||||
Il est important de se rappeler que l'IHH a deux seuils :
|
"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
|
"* 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
|
"* 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.
|
"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.
|
"Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils."
|
||||||
""")
|
])))
|
||||||
st.markdown("---")
|
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:
|
try:
|
||||||
lancer_visualisation_ihh_criticite(G_temp)
|
lancer_visualisation_ihh_criticite(G_temp)
|
||||||
except Exception as e:
|
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:
|
try:
|
||||||
lancer_visualisation_ihh_ivc(G_temp_ivc)
|
lancer_visualisation_ihh_ivc(G_temp_ivc)
|
||||||
except Exception as e:
|
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}")
|
||||||
|
|||||||
276
assets/locales/en.json
Normal file
276
assets/locales/en.json
Normal 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": "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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
276
assets/locales/fr.json
Normal file
276
assets/locales/fr.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,11 +7,16 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
body, html {
|
body,
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
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;
|
background-color: var(--bg-color) !important;
|
||||||
color: var(--text-color) !important;
|
color: var(--text-color) !important;
|
||||||
}
|
}
|
||||||
@ -47,14 +52,20 @@ body, .stApp, .block-container {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
section:not([data-testid="stSidebar"]) button[data-testid="stBaseButton-primary"],
|
section:not([data-testid="stSidebar"])
|
||||||
section:not([data-testid="stSidebar"]) button[data-testid="stBaseButton-secondary"] {
|
button[data-testid="stBaseButton-primary"],
|
||||||
|
section:not([data-testid="stSidebar"])
|
||||||
|
button[data-testid="stBaseButton-secondary"] {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
background: darkgreen !important;
|
background: darkgreen !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
section:not([data-testid="stSidebar"]) button[data-testid="stBaseButton-primary"] p,
|
section:not([data-testid="stSidebar"])
|
||||||
section:not([data-testid="stSidebar"]) button[data-testid="stBaseButton-secondary"] p {
|
button[data-testid="stBaseButton-primary"]
|
||||||
|
p,
|
||||||
|
section:not([data-testid="stSidebar"])
|
||||||
|
button[data-testid="stBaseButton-secondary"]
|
||||||
|
p {
|
||||||
color: white !important;
|
color: white !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,7 +115,9 @@ section:not([data-testid="stSidebar"]) div[role="radiogroup"] > label p {
|
|||||||
color: var(--radio-text) !important;
|
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;
|
background-color: var(--radio-selected-bg) !important;
|
||||||
color: var(--radio-selected-text) !important;
|
color: var(--radio-selected-text) !important;
|
||||||
}
|
}
|
||||||
@ -118,6 +131,10 @@ section[data-testid="stFileUploaderDropzone"] {
|
|||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
section:not([data-testid="stSidebar"]) div[data-testid="stSelectbox"] p,
|
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="stMultiSelect"] p,
|
||||||
section:not([data-testid="stSidebar"]) div[data-testid="stRadio"] p,
|
section:not([data-testid="stSidebar"]) div[data-testid="stRadio"] p,
|
||||||
@ -208,7 +225,8 @@ table {
|
|||||||
margin-bottom: 1.5em;
|
margin-bottom: 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
th, td {
|
th,
|
||||||
|
td {
|
||||||
border: 1px solid var(--table-border) !important;
|
border: 1px solid var(--table-border) !important;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-align: left;
|
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) --- */
|
/* --- 7.1 File Uploader --- */
|
||||||
/* Hide original "Drag and drop file here" text */
|
/* File uploader styles intentionally left empty */
|
||||||
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.2 Graphiques --- */
|
/* --- 7.2 Graphiques --- */
|
||||||
.stPlotlyChart text {
|
.stPlotlyChart text {
|
||||||
@ -312,7 +285,9 @@ details {
|
|||||||
border-color: var(--details-border) !important;
|
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;
|
color: var(--paragraph-color) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,7 +296,8 @@ section:not([data-testid="stSidebar"]) hr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* --- 7.4 Conteneurs de commentaires et tickets --- */
|
/* --- 7.4 Conteneurs de commentaires et tickets --- */
|
||||||
.conteneur_commentaire, .conteneur_ticket {
|
.conteneur_commentaire,
|
||||||
|
.conteneur_ticket {
|
||||||
background: var(--background-color);
|
background: var(--background-color);
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@ -329,12 +305,14 @@ section:not([data-testid="stSidebar"]) hr {
|
|||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentaire_auteur, .ticket_auteur {
|
.commentaire_auteur,
|
||||||
|
.ticket_auteur {
|
||||||
color: var(--text-color) !important;
|
color: var(--text-color) !important;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.commentaire_contenu, .ticket_contenu {
|
.commentaire_contenu,
|
||||||
|
.ticket_contenu {
|
||||||
color: var(--text-color) !important;
|
color: var(--text-color) !important;
|
||||||
margin: 0.5rem 0 0;
|
margin: 0.5rem 0 0;
|
||||||
}
|
}
|
||||||
@ -366,4 +344,4 @@ div.stElementContainer.element-container.st-key-nom_utilisateur {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import streamlit as st
|
|||||||
import requests
|
import requests
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
def initialiser_logger():
|
def initialiser_logger():
|
||||||
LOG_FILE_PATH = "/var/log/fabnum-auth.log"
|
LOG_FILE_PATH = "/var/log/fabnum-auth.log"
|
||||||
@ -19,10 +20,11 @@ def initialiser_logger():
|
|||||||
|
|
||||||
def connexion():
|
def connexion():
|
||||||
if "logged_in" not in st.session_state or not st.session_state.logged_in:
|
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"""
|
||||||
<section role="region" aria-label="region-authentification">
|
<section role="region" aria-label="region-authentification">
|
||||||
<div role="region" aria-labelledby="Authentification">
|
<div role="region" aria-labelledby="Authentification">
|
||||||
<p id="Authentification" class="decorative-heading">Authentification</p>
|
<p id="Authentification" class="decorative-heading">{auth_title}</p>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
|
GITEA_URL = "https://fabnum-git.peccini.fr/api/v1"
|
||||||
@ -40,9 +42,9 @@ def connexion():
|
|||||||
with st.form("auth_form"):
|
with st.form("auth_form"):
|
||||||
# Ajout d'un champ identifiant fictif pour activer l'autocomplétion navigateur
|
# 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
|
# 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")
|
identifiant = st.text_input(str(_("auth.username", "Identifiant_token")), value="fabnum-connexion", key="nom_utilisateur")
|
||||||
token = st.text_input("Token d'accès personnel Gitea", type="password")
|
token = st.text_input(str(_("auth.token", "Token d'accès personnel Gitea")), type="password")
|
||||||
submitted = st.form_submit_button("Se connecter")
|
submitted = st.form_submit_button(str(_("auth.login", "Se connecter")))
|
||||||
|
|
||||||
if submitted and token:
|
if submitted and token:
|
||||||
erreur = True
|
erreur = True
|
||||||
@ -76,11 +78,11 @@ def connexion():
|
|||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
except requests.RequestException:
|
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:
|
if erreur:
|
||||||
logger.warning(f"Accès refusé pour tentative avec token depuis IP {ip}")
|
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("""
|
st.html("""
|
||||||
</div>
|
</div>
|
||||||
@ -90,21 +92,22 @@ def connexion():
|
|||||||
|
|
||||||
def bouton_deconnexion():
|
def bouton_deconnexion():
|
||||||
if st.session_state.get("logged_in", False):
|
if st.session_state.get("logged_in", False):
|
||||||
st.html("""
|
auth_title = str(_("auth.title", "Authentification"))
|
||||||
|
st.html(f"""
|
||||||
<section role="region" aria-label="region-authentification">
|
<section role="region" aria-label="region-authentification">
|
||||||
<div role="region" aria-labelledby="Authentification">
|
<div role="region" aria-labelledby="Authentification">
|
||||||
<p id="Authentification" class="decorative-heading">Authentification</p>
|
<p id="Authentification" class="decorative-heading">{auth_title}</p>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
st.sidebar.markdown(f"Connecté en tant que `{st.session_state.username}`")
|
st.sidebar.markdown(f"{str(_('auth.logged_as', 'Connecté en tant que'))} `{st.session_state.username}`")
|
||||||
if st.sidebar.button("Se déconnecter"):
|
if st.sidebar.button(str(_("auth.logout", "Se déconnecter"))):
|
||||||
st.session_state.logged_in = False
|
st.session_state.logged_in = False
|
||||||
st.session_state.username = ""
|
st.session_state.username = ""
|
||||||
st.session_state.token = ""
|
st.session_state.token = ""
|
||||||
st.success("Déconnecté avec succès.")
|
st.success(str(_("auth.success", "Déconnecté avec succès.")))
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
st.html("""
|
st.html("""
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
""")
|
""")
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
|
|
||||||
def afficher_pied_de_page():
|
def afficher_pied_de_page():
|
||||||
@ -6,15 +7,15 @@ def afficher_pied_de_page():
|
|||||||
<section role="region" aria-label="Contenu principal" id="main-content">
|
<section role="region" aria-label="Contenu principal" id="main-content">
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
st.markdown("""
|
st.markdown(f"""
|
||||||
<div role='contentinfo' aria-labelledby='footer-appli' class='wide-footer'>
|
<div role='contentinfo' aria-labelledby='footer-appli' class='wide-footer'>
|
||||||
<div class='info-footer'>
|
<div class='info-footer'>
|
||||||
<p id='footer-appli' class='info-footer'>
|
<p id='footer-appli' class='info-footer'>
|
||||||
Fabnum © 2025 – <a href='mailto:stephan-pro@peccini.fr'>Contact</a> – Licence <a href='https://creativecommons.org/licenses/by-nc-nd/4.0/deed.fr' target='_blank'>CC BY-NC-ND</a>
|
{_("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>
|
||||||
</p>
|
</p>
|
||||||
<p class='footer-note'>
|
<p class='footer-note'>
|
||||||
🌱 Calculs CO₂ via <a href='https://www.thegreenwebfoundation.org/' target='_blank'>The Green Web Foundation</a><br>
|
{_("footer.eco_note", "🌱 Calculs CO₂ via")} <a href='https://www.thegreenwebfoundation.org/' target='_blank'>{_("footer.eco_provider", "The Green Web Foundation")}</a><br>
|
||||||
🚀 Propulsé par <a href='https://streamlit.io/' target='_blank'>Streamlit</a>
|
{_("footer.powered_by", "🚀 Propulsé par")} <a href='https://streamlit.io/' target='_blank'>{_("footer.powered_by_name", "Streamlit")}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,18 +1,19 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
from config import ENV
|
from config import ENV
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
|
|
||||||
def afficher_entete():
|
def afficher_entete():
|
||||||
header = """
|
header = f"""
|
||||||
<header role="banner" aria-labelledby="entete-header">
|
<header role="banner" aria-labelledby="entete-header">
|
||||||
<div class='wide-header'>
|
<div class='wide-header'>
|
||||||
<p id='entete-header' class='titre-header'>FabNum - Chaîne de fabrication du numérique</p>
|
<p id='entete-header' class='titre-header'>{_("header.title", "FabNum - Chaîne de fabrication du numérique")}</p>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if ENV == "dev":
|
if ENV == "dev":
|
||||||
header += "<p>🔧 Vous êtes dans l'environnement de développement.</p>"
|
header += f"<p>🔧 {_("app.dev_mode", "Vous êtes dans l'environnement de développement.")}</p>"
|
||||||
else:
|
else:
|
||||||
header += "<p>Parcours de l'écosystème et identification des vulnérabilités.</p>"
|
header += f"<p>{_("header.subtitle", "Parcours de l'écosystème et identification des vulnérabilités.")}</p>"
|
||||||
|
|
||||||
header += """
|
header += """
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,26 +1,35 @@
|
|||||||
import streamlit as st
|
import streamlit as st
|
||||||
from components.connexion import connexion, bouton_deconnexion
|
from components.connexion import connexion, bouton_deconnexion
|
||||||
import streamlit.components.v1 as components
|
import streamlit.components.v1 as components
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
|
|
||||||
def afficher_menu():
|
def afficher_menu():
|
||||||
with st.sidebar:
|
with st.sidebar:
|
||||||
st.markdown("""
|
st.markdown(f"""
|
||||||
<nav role="navigation" aria-label="Menu principal">
|
<nav role="navigation" aria-label="{str(_('sidebar.menu', 'Menu principal'))}">
|
||||||
<div role="region" aria-label="Navigation principale" class="onglets-accessibles">
|
<div role="region" aria-label="{str(_('sidebar.navigation', 'Navigation principale'))}" class="onglets-accessibles">
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# Définir la variable instructions_text une seule fois en haut de la fonction
|
||||||
|
instructions_text = str(_("navigation.instructions", "Instructions"))
|
||||||
if "onglet" not in st.session_state:
|
if "onglet" not in st.session_state:
|
||||||
st.session_state.onglet = "Instructions"
|
st.session_state.onglet = instructions_text
|
||||||
|
|
||||||
onglet_choisi = None
|
onglet_choisi = None
|
||||||
onglets = ["Instructions", "Personnalisation", "Analyse", "Visualisations", "Fiches"]
|
onglets = [
|
||||||
|
str(_("navigation.instructions", "Instructions")),
|
||||||
|
str(_("navigation.personnalisation", "Personnalisation")),
|
||||||
|
str(_("navigation.analyse", "Analyse")),
|
||||||
|
str(_("navigation.visualisations", "Visualisations")),
|
||||||
|
str(_("navigation.fiches", "Fiches"))
|
||||||
|
]
|
||||||
|
|
||||||
for nom in onglets:
|
for nom in onglets:
|
||||||
if st.session_state.onglet == nom:
|
if st.session_state.onglet == nom:
|
||||||
st.markdown(f'<div class="bouton-fictif">{nom}</div>', unsafe_allow_html=True)
|
st.markdown(f'<div class="bouton-fictif">{nom}</div>', unsafe_allow_html=True)
|
||||||
else:
|
else:
|
||||||
if st.button(nom):
|
if st.button(str(nom)):
|
||||||
onglet_choisi = nom
|
onglet_choisi = nom
|
||||||
|
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
@ -34,30 +43,42 @@ def afficher_menu():
|
|||||||
# Pour éviter de perdre les informations dans les formulaires,
|
# Pour éviter de perdre les informations dans les formulaires,
|
||||||
# le changement de thème n'est proposé que si l'utilisateur est sur l'onglet "Instructions"
|
# le changement de thème n'est proposé que si l'utilisateur est sur l'onglet "Instructions"
|
||||||
#
|
#
|
||||||
if st.session_state.onglet == "Instructions":
|
if st.session_state.onglet == instructions_text:
|
||||||
if "theme_mode" not in st.session_state:
|
if "theme_mode" not in st.session_state:
|
||||||
st.session_state.theme_mode = "Clair"
|
st.session_state.theme_mode = str(_("sidebar.theme_light", "Clair"))
|
||||||
|
|
||||||
st.markdown("""
|
theme_title = str(_("sidebar.theme", "Thème"))
|
||||||
|
st.markdown(f"""
|
||||||
<section role="region" aria-label="region-theme">
|
<section role="region" aria-label="region-theme">
|
||||||
<div role="region" aria-labelledby="Theme">
|
<div role="region" aria-labelledby="Theme">
|
||||||
<p id="Theme" class="decorative-heading">Thème</p>
|
<p id="Theme" class="decorative-heading">{theme_title}</p>
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
theme = st.radio("Thème", ["Clair", "Sombre"], index=["Clair", "Sombre"].index(st.session_state.theme_mode), horizontal=True, label_visibility="hidden")
|
theme_options = [
|
||||||
|
str(_("sidebar.theme_light", "Clair")),
|
||||||
|
str(_("sidebar.theme_dark", "Sombre"))
|
||||||
|
]
|
||||||
|
theme = st.radio(
|
||||||
|
str(_("sidebar.theme", "Thème")),
|
||||||
|
theme_options,
|
||||||
|
index=theme_options.index(st.session_state.theme_mode),
|
||||||
|
horizontal=True,
|
||||||
|
label_visibility="hidden"
|
||||||
|
)
|
||||||
|
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
</nav>""", unsafe_allow_html=True)
|
</nav>""", unsafe_allow_html=True)
|
||||||
else :
|
else :
|
||||||
st.markdown("""
|
theme_title = str(_("sidebar.theme", "Thème"))
|
||||||
|
st.markdown(f"""
|
||||||
<section role="region" aria-label="region-theme">
|
<section role="region" aria-label="region-theme">
|
||||||
<div role="region" aria-labelledby="Theme">
|
<div role="region" aria-labelledby="Theme">
|
||||||
<p id="Theme" class="decorative-heading">Thème</p>
|
<p id="Theme" class="decorative-heading">{theme_title}</p>
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
st.info("Le changement de thème ne peut se faire que depuis l'onglet Instructions.")
|
st.info(str(_("sidebar.theme_instructions_only", "Le changement de thème ne peut se faire que depuis l'onglet Instructions.")))
|
||||||
|
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
<hr />
|
<hr />
|
||||||
@ -80,6 +101,9 @@ def afficher_menu():
|
|||||||
|
|
||||||
|
|
||||||
def afficher_impact(total_bytes):
|
def afficher_impact(total_bytes):
|
||||||
|
impact_label = str(_("sidebar.impact", "Impact environnemental"))
|
||||||
|
loading_text = str(_("sidebar.loading", "Chargement en cours…"))
|
||||||
|
|
||||||
with st.sidebar:
|
with st.sidebar:
|
||||||
components.html(f"""
|
components.html(f"""
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
@ -115,9 +139,9 @@ def afficher_impact(total_bytes):
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<hr />
|
<hr />
|
||||||
<div role="region" aria-label="Impact environnemental" class="impact-environnement">
|
<div role="region" aria-label="{impact_label}" class="impact-environnement">
|
||||||
<p class="decorative-heading">Impact environnemental</p>
|
<p class="decorative-heading">{impact_label}</p>
|
||||||
<p><span id="network-usage">Chargement en cours…</span></p>
|
<p><span id="network-usage">{loading_text}</span></p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
63
fabnum.py
63
fabnum.py
@ -8,6 +8,9 @@ from utils.gitea import (
|
|||||||
charger_instructions_depuis_gitea
|
charger_instructions_depuis_gitea
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Import du module de traductions
|
||||||
|
from utils.translations import init_translations, _, set_language
|
||||||
|
|
||||||
def afficher_instructions_avec_expanders(markdown_content):
|
def afficher_instructions_avec_expanders(markdown_content):
|
||||||
"""
|
"""
|
||||||
Affiche le contenu markdown avec les sections de niveau 2 (## Titre) dans des expanders
|
Affiche le contenu markdown avec les sections de niveau 2 (## Titre) dans des expanders
|
||||||
@ -73,6 +76,12 @@ st.set_page_config(
|
|||||||
initial_sidebar_state="expanded"
|
initial_sidebar_state="expanded"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Initialisation des traductions (langue française par défaut)
|
||||||
|
init_translations()
|
||||||
|
|
||||||
|
# Pour tester d'autres langues, décommenter cette ligne :
|
||||||
|
set_language("fr")
|
||||||
|
|
||||||
session_id = st.context.headers.get("x-session-id")
|
session_id = st.context.headers.get("x-session-id")
|
||||||
|
|
||||||
def get_total_bytes_for_session(session_id):
|
def get_total_bytes_for_session(session_id):
|
||||||
@ -90,35 +99,36 @@ def get_total_bytes_for_session(session_id):
|
|||||||
return total_bytes
|
return total_bytes
|
||||||
|
|
||||||
def charger_theme():
|
def charger_theme():
|
||||||
# Une seule lecture du fichier, mais injection à chaque run
|
# Chargement des fichiers CSS (une seule fois)
|
||||||
if "base_css_content" not in st.session_state:
|
if "base_css_content" not in st.session_state:
|
||||||
with open("assets/styles/base.css") as f:
|
with open("assets/styles/base.css") as f:
|
||||||
st.session_state["base_css_content"] = f.read()
|
st.session_state["base_css_content"] = f.read()
|
||||||
|
|
||||||
st.markdown(f"<style>{st.session_state['base_css_content']}</style>", unsafe_allow_html=True)
|
if "theme_css_content_light" not in st.session_state:
|
||||||
|
|
||||||
# Chargement initial des thèmes (variables CSS uniquement)
|
|
||||||
if "theme_css_content_clair" not in st.session_state:
|
|
||||||
with open("assets/styles/theme-light.css") as f:
|
with open("assets/styles/theme-light.css") as f:
|
||||||
st.session_state["theme_css_content_clair"] = f.read()
|
st.session_state["theme_css_content_light"] = f.read()
|
||||||
|
|
||||||
if "theme_css_content_sombre" not in st.session_state:
|
if "theme_css_content_dark" not in st.session_state:
|
||||||
with open("assets/styles/theme-dark.css") as f:
|
with open("assets/styles/theme-dark.css") as f:
|
||||||
st.session_state["theme_css_content_sombre"] = f.read()
|
st.session_state["theme_css_content_dark"] = f.read()
|
||||||
|
|
||||||
# Thème en cours
|
# Mappage des noms traduits vers les noms internes
|
||||||
current_theme = st.session_state.get("theme_mode", "Clair").lower()
|
theme_mapping = {
|
||||||
|
"clair": "light",
|
||||||
|
"sombre": "dark",
|
||||||
|
"light": "light",
|
||||||
|
"dark": "dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Thème en cours (conversion du nom traduit vers l'identifiant interne)
|
||||||
|
current_theme_display = st.session_state.get("theme_mode", "Clair").lower()
|
||||||
|
current_theme = theme_mapping.get(current_theme_display, "light") # Par défaut light si non trouvé
|
||||||
theme_css = st.session_state[f"theme_css_content_{current_theme}"]
|
theme_css = st.session_state[f"theme_css_content_{current_theme}"]
|
||||||
|
|
||||||
# Injection des variables du thème
|
# Injection des CSS dans le bon ordre
|
||||||
|
# 1. D'abord les variables du thème
|
||||||
st.markdown(f"<style>{theme_css}</style>", unsafe_allow_html=True)
|
st.markdown(f"<style>{theme_css}</style>", unsafe_allow_html=True)
|
||||||
|
# 2. Ensuite le CSS de base
|
||||||
# Chargement unique du CSS principal (base.css)
|
|
||||||
if "base_css_content" not in st.session_state:
|
|
||||||
with open("assets/styles/base.css") as f:
|
|
||||||
st.session_state["base_css_content"] = f.read()
|
|
||||||
|
|
||||||
# Injection du style principal basé sur les variables
|
|
||||||
st.markdown(f"<style>{st.session_state['base_css_content']}</style>", unsafe_allow_html=True)
|
st.markdown(f"<style>{st.session_state['base_css_content']}</style>", unsafe_allow_html=True)
|
||||||
|
|
||||||
def ouvrir_page():
|
def ouvrir_page():
|
||||||
@ -143,12 +153,19 @@ ouvrir_page()
|
|||||||
|
|
||||||
dot_file_path = None
|
dot_file_path = None
|
||||||
|
|
||||||
if st.session_state.onglet == "Instructions":
|
# 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")
|
||||||
|
|
||||||
|
if st.session_state.onglet == instructions_tab:
|
||||||
markdown_content = charger_instructions_depuis_gitea(INSTRUCTIONS)
|
markdown_content = charger_instructions_depuis_gitea(INSTRUCTIONS)
|
||||||
if markdown_content:
|
if markdown_content:
|
||||||
afficher_instructions_avec_expanders(markdown_content)
|
afficher_instructions_avec_expanders(markdown_content)
|
||||||
|
|
||||||
elif st.session_state.onglet == "Fiches":
|
elif st.session_state.onglet == fiches_tab:
|
||||||
interface_fiches()
|
interface_fiches()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -156,13 +173,13 @@ else:
|
|||||||
# Le graphe n'est pas nécessaire pour Instructions ou Fiches
|
# Le graphe n'est pas nécessaire pour Instructions ou Fiches
|
||||||
G_temp, G_temp_ivc, dot_file_path = charger_graphe()
|
G_temp, G_temp_ivc, dot_file_path = charger_graphe()
|
||||||
|
|
||||||
if dot_file_path and st.session_state.onglet == "Analyse":
|
if dot_file_path and st.session_state.onglet == analyse_tab:
|
||||||
interface_analyse(G_temp)
|
interface_analyse(G_temp)
|
||||||
|
|
||||||
elif dot_file_path and st.session_state.onglet == "Visualisations":
|
elif dot_file_path and st.session_state.onglet == visualisations_tab:
|
||||||
interface_visualisations(G_temp, G_temp_ivc)
|
interface_visualisations(G_temp, G_temp_ivc)
|
||||||
|
|
||||||
elif dot_file_path and st.session_state.onglet == "Personnalisation":
|
elif dot_file_path and st.session_state.onglet == personnalisation_tab:
|
||||||
G_temp = interface_personnalisation(G_temp)
|
G_temp = interface_personnalisation(G_temp)
|
||||||
|
|
||||||
fermer_page()
|
fermer_page()
|
||||||
|
|||||||
@ -1,6 +1,17 @@
|
|||||||
#
|
altair==5.5.0
|
||||||
# This file is autogenerated by pip-compile with Python 3.12
|
beautifulsoup4==4.13.4
|
||||||
# by the following command:
|
Jinja2==3.1.6
|
||||||
#
|
latex2mathml==3.78.0
|
||||||
# pip-compile requirements.in
|
Markdown==3.8
|
||||||
#
|
networkx==3.4.2
|
||||||
|
numpy==2.2.5
|
||||||
|
pandas==2.2.3
|
||||||
|
plotly==6.0.1
|
||||||
|
pypandoc==1.15
|
||||||
|
python-dotenv==1.1.0
|
||||||
|
python_dateutil==2.9.0.post0
|
||||||
|
python_frontmatter==1.1.0
|
||||||
|
PyYAML==6.0.1
|
||||||
|
Requests==2.32.3
|
||||||
|
streamlit==1.45.1
|
||||||
|
pygraphviz==1.14
|
||||||
|
|||||||
84
utils/translations.py
Normal file
84
utils/translations.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import streamlit as st
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
# Configuration du logger
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger("translations")
|
||||||
|
|
||||||
|
def load_translations(lang="fr"):
|
||||||
|
"""
|
||||||
|
Charge les traductions depuis le fichier JSON correspondant à la langue spécifiée.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lang (str): Code de langue (par défaut: "fr" pour français)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Dictionnaire des traductions ou un dictionnaire vide en cas d'erreur
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
file_path = os.path.join("assets", "locales", f"{lang}.json")
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
logger.warning(f"Fichier de traduction non trouvé: {file_path}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
translations = json.load(f)
|
||||||
|
logger.info(f"Traductions chargées: {lang}")
|
||||||
|
return translations
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur lors du chargement des traductions: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_translation(key, default=None):
|
||||||
|
"""
|
||||||
|
Récupère une traduction par sa clé.
|
||||||
|
Les clés peuvent être hiérarchiques, séparées par des points.
|
||||||
|
Exemple: "header.title" pour accéder à translations["header"]["title"]
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): Clé de traduction (peut être hiérarchique comme "header.title")
|
||||||
|
default (str, optional): Valeur par défaut si la traduction n'est pas trouvée
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Texte traduit ou valeur par défaut
|
||||||
|
"""
|
||||||
|
# Initialiser les traductions si nécessaire
|
||||||
|
if "translations" not in st.session_state:
|
||||||
|
st.session_state.translations = load_translations("fr")
|
||||||
|
st.session_state.lang = "fr"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Parcourir la hiérarchie des clés
|
||||||
|
keys = key.split(".")
|
||||||
|
current = st.session_state.translations
|
||||||
|
|
||||||
|
for k in keys:
|
||||||
|
if not isinstance(current, dict) or k not in current:
|
||||||
|
return default if default is not None else key
|
||||||
|
current = current[k]
|
||||||
|
|
||||||
|
return current
|
||||||
|
|
||||||
|
def set_language(lang="fr"):
|
||||||
|
"""
|
||||||
|
Force l'utilisation d'une langue spécifique.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lang (str): Code de langue à utiliser
|
||||||
|
"""
|
||||||
|
st.session_state.lang = lang
|
||||||
|
st.session_state.translations = load_translations(lang)
|
||||||
|
|
||||||
|
# Initialiser la langue française par défaut
|
||||||
|
def init_translations():
|
||||||
|
"""Initialise les traductions avec la langue française"""
|
||||||
|
if "translations" not in st.session_state:
|
||||||
|
set_language("fr")
|
||||||
|
|
||||||
|
# Raccourci pour get_translation
|
||||||
|
_ = get_translation
|
||||||
@ -3,13 +3,19 @@ import altair as alt
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from utils.translations import _
|
||||||
|
|
||||||
|
|
||||||
def afficher_graphique_altair(df):
|
def afficher_graphique_altair(df):
|
||||||
ordre_personnalise = ['Assemblage', 'Fabrication', 'Traitement', 'Extraction']
|
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"))
|
||||||
|
]
|
||||||
categories = [cat for cat in ordre_personnalise if cat in df['categorie'].unique()]
|
categories = [cat for cat in ordre_personnalise if cat in df['categorie'].unique()]
|
||||||
for cat in categories:
|
for cat in categories:
|
||||||
st.markdown(f"### {cat}")
|
st.markdown(f"### {str(cat)}")
|
||||||
df_cat = df[df['categorie'] == cat].copy()
|
df_cat = df[df['categorie'] == cat].copy()
|
||||||
|
|
||||||
coord_pairs = list(zip(df_cat['ihh_pays'].round(1), df_cat['ihh_acteurs'].round(1)))
|
coord_pairs = list(zip(df_cat['ihh_pays'].round(1), df_cat['ihh_acteurs'].round(1)))
|
||||||
@ -36,8 +42,8 @@ def afficher_graphique_altair(df):
|
|||||||
df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5
|
df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5
|
||||||
|
|
||||||
base = alt.Chart(df_cat).encode(
|
base = alt.Chart(df_cat).encode(
|
||||||
x=alt.X('ihh_pays:Q', title='IHH Pays (%)'),
|
x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries", "IHH Pays (%)"))),
|
||||||
y=alt.Y('ihh_acteurs:Q', title='IHH Acteurs (%)'),
|
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),
|
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']))
|
color=alt.Color('criticite_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred']))
|
||||||
)
|
)
|
||||||
@ -64,7 +70,7 @@ def afficher_graphique_altair(df):
|
|||||||
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
|
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
|
||||||
width=500,
|
width=500,
|
||||||
height=400,
|
height=400,
|
||||||
title=f"Concentration et criticité – {cat}"
|
title=str(_("pages.visualisations.chart_titles.concentration_criticality", "Concentration et criticité – {0}")).format(str(cat))
|
||||||
).interactive()
|
).interactive()
|
||||||
|
|
||||||
st.altair_chart(chart, use_container_width=True)
|
st.altair_chart(chart, use_container_width=True)
|
||||||
@ -72,7 +78,7 @@ def afficher_graphique_altair(df):
|
|||||||
|
|
||||||
def creer_graphes(donnees):
|
def creer_graphes(donnees):
|
||||||
if not donnees:
|
if not donnees:
|
||||||
st.warning("Aucune donnée à afficher.")
|
st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à afficher.")))
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -103,8 +109,8 @@ def creer_graphes(donnees):
|
|||||||
df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5
|
df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5
|
||||||
|
|
||||||
base = alt.Chart(df).encode(
|
base = alt.Chart(df).encode(
|
||||||
x=alt.X('ihh_extraction:Q', title='IHH Extraction (%)'),
|
x=alt.X('ihh_extraction:Q', title=str(_("pages.visualisations.axis_titles.ihh_extraction", "IHH Extraction (%)"))),
|
||||||
y=alt.Y('ihh_reserves:Q', title='IHH Réserves (%)'),
|
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),
|
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'])),
|
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']
|
tooltip=['nom:N', 'ivc:Q', 'ihh_extraction:Q', 'ihh_reserves:Q']
|
||||||
@ -132,13 +138,13 @@ def creer_graphes(donnees):
|
|||||||
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
|
chart = (points + lines + labels + hline_15 + hline_25 + vline_15 + vline_25).properties(
|
||||||
width=600,
|
width=600,
|
||||||
height=500,
|
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()
|
).interactive()
|
||||||
|
|
||||||
st.altair_chart(chart, use_container_width=True)
|
st.altair_chart(chart, use_container_width=True)
|
||||||
|
|
||||||
except Exception as e:
|
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):
|
def lancer_visualisation_ihh_criticite(graph):
|
||||||
@ -152,11 +158,11 @@ def lancer_visualisation_ihh_criticite(graph):
|
|||||||
|
|
||||||
df = recuperer_donnees(graph, noeuds)
|
df = recuperer_donnees(graph, noeuds)
|
||||||
if df.empty:
|
if df.empty:
|
||||||
st.warning("Aucune donnée à visualiser.")
|
st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à visualiser.")))
|
||||||
else:
|
else:
|
||||||
afficher_graphique_altair(df)
|
afficher_graphique_altair(df)
|
||||||
except Exception as e:
|
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):
|
def lancer_visualisation_ihh_ivc(graph):
|
||||||
@ -171,4 +177,4 @@ def lancer_visualisation_ihh_ivc(graph):
|
|||||||
data = recuperer_donnees_2(graph, noeuds_niveau_2)
|
data = recuperer_donnees_2(graph, noeuds_niveau_2)
|
||||||
creer_graphes(data)
|
creer_graphes(data)
|
||||||
except Exception as e:
|
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}")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user