diff --git a/app/analyse/interface.py b/app/analyse/interface.py
index 3eb0078..093b56e 100644
--- a/app/analyse/interface.py
+++ b/app/analyse/interface.py
@@ -31,11 +31,11 @@ def preparer_graphe(G):
def selectionner_niveaux():
"""Interface pour sélectionner les niveaux de départ et d'arrivée."""
- st.markdown(f"## {str(_('pages.analyse.selection_nodes', 'Sélection des nœuds de départ et d\'arrivée'))}")
- valeur_defaut = str(_("pages.analyse.select_level", "-- Sélectionner un niveau --"))
+ st.markdown(f"## {str(_('pages.analyse.selection_nodes'))}")
+ valeur_defaut = str(_("pages.analyse.select_level"))
niveau_choix = [valeur_defaut] + list(niveau_labels.values())
- niveau_depart = st.selectbox(str(_("pages.analyse.start_level", "Niveau de départ")), niveau_choix, key="analyse_niveau_depart")
+ niveau_depart = st.selectbox(str(_("pages.analyse.start_level")), niveau_choix, key="analyse_niveau_depart")
if niveau_depart == valeur_defaut:
return None, None
@@ -43,7 +43,7 @@ def selectionner_niveaux():
niveaux_arrivee_possibles = [v for k, v in niveau_labels.items() if k > niveau_depart_int]
niveaux_arrivee_choix = [valeur_defaut] + niveaux_arrivee_possibles
- analyse_niveau_arrivee = st.selectbox(str(_("pages.analyse.end_level", "Niveau d'arrivée")), niveaux_arrivee_choix, key="analyse_niveau_arrivee")
+ analyse_niveau_arrivee = st.selectbox(str(_("pages.analyse.end_level")), niveaux_arrivee_choix, key="analyse_niveau_arrivee")
if analyse_niveau_arrivee == valeur_defaut:
return niveau_depart_int, None
@@ -55,7 +55,7 @@ def selectionner_minerais(G, niveau_depart, niveau_arrivee):
"""Interface pour sélectionner les minerais si nécessaire."""
minerais_selection = None
if niveau_depart < 2 < niveau_arrivee:
- st.markdown(f"### {str(_('pages.analyse.select_minerals', 'Sélectionner un ou plusieurs minerais'))}")
+ st.markdown(f"### {str(_('pages.analyse.select_minerals'))}")
# Tous les nœuds de niveau 2 (minerai)
minerais_nodes = sorted([
n for n, d in G.nodes(data=True)
@@ -63,7 +63,7 @@ def selectionner_minerais(G, niveau_depart, niveau_arrivee):
])
minerais_selection = st.multiselect(
- str(_("pages.analyse.filter_by_minerals", "Filtrer par minerais (optionnel)")),
+ str(_("pages.analyse.filter_by_minerals")),
minerais_nodes,
key="analyse_minerais"
)
@@ -74,15 +74,15 @@ def selectionner_minerais(G, niveau_depart, niveau_arrivee):
def selectionner_noeuds(G, niveaux_temp, niveau_depart, niveau_arrivee):
"""Interface pour sélectionner les nœuds spécifiques de départ et d'arrivée."""
st.markdown("---")
- st.markdown(f"## {str(_('pages.analyse.fine_selection', 'Sélection fine des items'))}")
+ st.markdown(f"## {str(_('pages.analyse.fine_selection'))}")
depart_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_depart]
arrivee_nodes = [n for n in G.nodes() if niveaux_temp.get(n) == niveau_arrivee]
- noeuds_depart = st.multiselect(str(_("pages.analyse.filter_start_nodes", "Filtrer par noeuds de départ (optionnel)")),
+ noeuds_depart = st.multiselect(str(_("pages.analyse.filter_start_nodes")),
sorted(depart_nodes),
key="analyse_noeuds_depart")
- noeuds_arrivee = st.multiselect(str(_("pages.analyse.filter_end_nodes", "Filtrer par noeuds d'arrivée (optionnel)")),
+ noeuds_arrivee = st.multiselect(str(_("pages.analyse.filter_end_nodes")),
sorted(arrivee_nodes),
key="analyse_noeuds_arrivee")
@@ -95,26 +95,26 @@ def selectionner_noeuds(G, niveaux_temp, niveau_depart, niveau_arrivee):
def configurer_filtres_vulnerabilite():
"""Interface pour configurer les filtres de vulnérabilité."""
st.markdown("---")
- st.markdown(f"## {str(_('pages.analyse.vulnerability_filters', 'Sélection des filtres pour identifier les vulnérabilités'))}")
+ st.markdown(f"## {str(_('pages.analyse.vulnerability_filters'))}")
- filtrer_ics = st.checkbox(str(_("pages.analyse.filter_ics", "Filtrer les chemins contenant au moins minerai critique pour un composant (ICS > 66 %)")),
+ filtrer_ics = st.checkbox(str(_("pages.analyse.filter_ics")),
key="analyse_filtrer_ics")
- filtrer_ivc = st.checkbox(str(_("pages.analyse.filter_ivc", "Filtrer les chemins contenant au moins un minerai critique par rapport à la concurrence sectorielle (IVC > 30)")),
+ filtrer_ivc = st.checkbox(str(_("pages.analyse.filter_ivc")),
key="analyse_filtrer_ivc")
- filtrer_ihh = st.checkbox(str(_("pages.analyse.filter_ihh", "Filtrer les chemins contenant au moins une opération critique par rapport à la concentration géographique ou industrielle (IHH pays ou acteurs > 25)")),
+ filtrer_ihh = st.checkbox(str(_("pages.analyse.filter_ihh")),
key="analyse_filtrer_ihh")
ihh_type = "Pays"
if filtrer_ihh:
- ihh_type = st.radio(str(_("pages.analyse.apply_ihh_filter", "Appliquer le filtre IHH sur :")),
- [str(_("pages.analyse.countries", "Pays")), str(_("pages.analyse.actors", "Acteurs"))],
+ ihh_type = st.radio(str(_("pages.analyse.apply_ihh_filter")),
+ [str(_("pages.analyse.countries")), str(_("pages.analyse.actors"))],
horizontal=True,
key="analyse_ihh_type")
- filtrer_isg = st.checkbox(str(_("pages.analyse.filter_isg", "Filtrer les chemins contenant un pays instable (ISG ≥ 60)")),
+ filtrer_isg = st.checkbox(str(_("pages.analyse.filter_isg")),
key="analyse_filtrer_isg")
- logique_filtrage = st.radio(str(_("pages.analyse.filter_logic", "Logique de filtrage")),
- [str(_("pages.analyse.or", "OU")), str(_("pages.analyse.and", "ET"))],
+ logique_filtrage = st.radio(str(_("pages.analyse.filter_logic")),
+ [str(_("pages.analyse.or")), str(_("pages.analyse.and"))],
horizontal=True,
key="analyse_logique_filtrage")
@@ -122,16 +122,9 @@ def configurer_filtres_vulnerabilite():
def interface_analyse(G_temp):
- st.markdown(f"# {str(_('pages.analyse.title', 'Analyse du graphe'))}")
- with st.expander(str(_("pages.analyse.help", "Comment utiliser cet onglet ?")), expanded=False):
- st.markdown("\n".join(_("pages.analyse.help_content", [
- "1. Sélectionnez le niveau de départ (produit final, composant ou minerai)",
- "2. Choisissez le niveau d'arrivée souhaité",
- "3. Affinez votre sélection en spécifiant soit un ou des minerais à cibler spécifiquement ou des items précis à chaque niveau (optionnel)",
- "4. Définissez les critères d'analyse en sélectionnant les indices de vulnérabilité pertinents",
- "5. Choisissez le mode de combinaison des indices (ET/OU) selon votre besoin d'analyse",
- "6. Explorez le graphique généré en utilisant les contrôles de zoom et de déplacement ; vous pouvez basculer en mode plein écran pour le graphe"
- ])))
+ st.markdown(f"# {str(_('pages.analyse.title'))}")
+ with st.expander(str(_("pages.analyse.help")), expanded=False):
+ st.markdown("\n".join(_("pages.analyse.help_content")))
st.markdown("---")
try:
@@ -155,7 +148,7 @@ def interface_analyse(G_temp):
# Lancement de l'analyse
st.markdown("---")
- if st.button(str(_("pages.analyse.run_analysis", "Lancer l'analyse")), type="primary", key="analyse_lancer"):
+ if st.button(str(_("pages.analyse.run_analysis")), type="primary", key="analyse_lancer"):
afficher_sankey(
G_temp,
niveau_depart=niveau_depart,
@@ -172,4 +165,4 @@ def interface_analyse(G_temp):
)
except Exception as e:
- st.error(f"{str(_('errors.graph_preview_error', 'Erreur de prévisualisation du graphe :'))} {e}")
+ st.error(f"{str(_('errors.graph_preview_error'))} {e}")
diff --git a/app/analyse/sankey.py b/app/analyse/sankey.py
index 6a2dec8..5878064 100644
--- a/app/analyse/sankey.py
+++ b/app/analyse/sankey.py
@@ -187,16 +187,22 @@ def couleur_criticite(p):
def edge_info(G, u, v):
"""Génère l'info-bulle pour un lien"""
+ # Liste d'attributs à exclure des infobulles des liens
+ attributs_exclus = ["poids", "color", "fontcolor"]
+
data = G.get_edge_data(u, v)
if not data:
- return f"{str(_('pages.analyse.sankey.relation', 'Relation'))} : {u} → {v}"
+ return f"{str(_('pages.analyse.sankey.relation'))} : {u} → {v}"
if isinstance(data, dict) and all(isinstance(k, int) for k in data):
data = data[0]
- base = [f"{k}: {v}" for k, v in data.items()]
- return f"{str(_('pages.analyse.sankey.relation', 'Relation'))} : {u} → {v}
" + "
".join(base)
+ base = [f"{k}: {v}" for k, v in data.items() if k not in attributs_exclus]
+ return f"{str(_('pages.analyse.sankey.relation'))} : {u} → {v}
" + "
".join(base)
def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
"""Prépare les données pour le graphique Sankey"""
+ # Liste d'attributs à exclure des infobulles des nœuds
+ node_attributs_exclus = ["fillcolor", "niveau"]
+
df_liens = pd.DataFrame(list(liens_chemins), columns=["source", "target"])
df_liens = df_liens.groupby(["source", "target"]).size().reset_index(name="value")
@@ -215,7 +221,7 @@ def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
noeuds_utilises.add(n)
df_liens["color"] = df_liens.apply(
- lambda row: couleur_criticite(row["criticite"]) if row["criticite"] > 0 else "gray",
+ lambda row: couleur_criticite(row["criticite"]) if row["criticite"] > 0 else "white",
axis=1
)
@@ -226,7 +232,7 @@ def preparer_donnees_sankey(G, liens_chemins, niveaux, chemins):
customdata = []
for n in sorted_nodes:
- info = [f"{k}: {v}" for k, v in G.nodes[n].items()]
+ info = [f"{k}: {v}" for k, v in G.nodes[n].items() if k not in node_attributs_exclus]
niveau = niveaux.get(n, 99)
# Ajout d'un ISG hérité si applicable
@@ -268,12 +274,17 @@ def creer_graphique_sankey(G, niveaux, df_liens, sorted_nodes, customdata, link_
value=values,
color=df_liens["color"].tolist(),
customdata=link_customdata,
- hovertemplate="%{customdata}"
+ hovertemplate="%{customdata}",
+ line=dict(
+ width=1, # Set fixed width to 3 pixels (or use 2 if preferred)
+ color="grey"
+ ),
+ arrowlen=10
)
))
fig.update_layout(
- title_text=str(_("pages.analyse.sankey.filtered_hierarchy", "Hiérarchie filtrée par niveaux et noeuds")),
+ title_text=str(_("pages.analyse.sankey.filtered_hierarchy")),
paper_bgcolor="white",
plot_bgcolor="white"
)
@@ -303,7 +314,7 @@ def exporter_graphe_filtre(G, liens_chemins):
with open(dot_path, encoding="utf-8") as f:
st.download_button(
- label=str(_("pages.analyse.sankey.download_dot", "Télécharger le fichier DOT filtré")),
+ label=str(_("pages.analyse.sankey.download_dot")),
data=f.read(),
file_name="graphe_filtré.dot",
mime="text/plain"
@@ -325,7 +336,7 @@ def afficher_sankey(
chemins = extraire_chemins_selon_criteres(G, niveaux, niveau_depart, noeuds_depart, noeuds_arrivee, minerais)
if not chemins:
- st.warning(str(_("pages.analyse.sankey.no_paths", "Aucun chemin trouvé pour les critères spécifiés.")))
+ st.warning(str(_("pages.analyse.sankey.no_paths")))
return
# Étape 3 : Filtrage des chemins selon les critères de vulnérabilité
@@ -335,12 +346,13 @@ def afficher_sankey(
)
if not liens_chemins:
- st.warning(str(_("pages.analyse.sankey.no_matching_paths", "Aucun chemin ne correspond aux critères.")))
+ st.warning(str(_("pages.analyse.sankey.no_matching_paths")))
return
# Étape 4 : Préparation des données pour le graphique Sankey
df_liens, sorted_nodes, customdata, link_customdata, node_indices = preparer_donnees_sankey(
- G, liens_chemins, niveaux, chemins_filtres if any([filtrer_ics, filtrer_ivc, filtrer_ihh, filtrer_isg]) else chemins
+ G, liens_chemins, niveaux,
+ chemins_filtres if any([filtrer_ics, filtrer_ivc, filtrer_ihh, filtrer_isg]) else chemins
)
# Étape 5 : Création et affichage du graphique Sankey
diff --git a/app/fiches/interface.py b/app/fiches/interface.py
index eebc042..44596c3 100644
--- a/app/fiches/interface.py
+++ b/app/fiches/interface.py
@@ -18,23 +18,9 @@ from .utils.fiche_utils import load_seuils, doit_regenerer_fiche
from .generer import generer_fiche
def interface_fiches():
- st.markdown(f"# {str(_('pages.fiches.title', 'Découverte des fiches'))}")
- with st.expander(str(_("pages.fiches.help", "Comment utiliser cet onglet ?")), expanded=False):
- st.markdown("\n".join([
- " " + line for line in _("pages.fiches.help_content", [
- "1. Parcourez la liste des fiches disponibles par catégorie",
- "2. Sélectionnez une fiche pour afficher son contenu complet",
- "3. Consultez les données détaillées, graphiques et analyses supplémentaires",
- "4. Utilisez ces informations pour approfondir votre compréhension des vulnérabilités identifiées",
- "",
- "Les catégories sont les suivantes :",
- "* Assemblage : opération d'assemblage des produits finaux à partir des composants",
- "* Connexe : opérations diverses nécessaires pour fabriquer le numérique, mais n'entrant pas directement dans sa composition",
- "* Criticités : indices utilisés pour identifier et évaluer les vulnérabilités",
- "* Fabrication : opération de fabrication des composants à partir de minerais",
- "* Minerai : description et opérations d'extraction et de traitement des minerais"
- ])
- ]))
+ st.markdown(f"# {str(_('pages.fiches.title'))}")
+ with st.expander(str(_("pages.fiches.help")), expanded=False):
+ st.markdown("\n".join(_("pages.fiches.help_content")))
st.markdown("---")
if "fiches_arbo" not in st.session_state:
@@ -42,24 +28,24 @@ def interface_fiches():
arbo = st.session_state.get("fiches_arbo", {})
if not arbo:
- st.warning(str(_("pages.fiches.no_files", "Aucune fiche disponible pour le moment.")))
+ st.warning(str(_("pages.fiches.no_files")))
return
dossiers = sorted(arbo.keys(), key=lambda x: x.lower())
dossier_choisi = st.selectbox(
- str(_("pages.fiches.choose_category", "Choisissez une catégorie de fiches")),
- [str(_("pages.fiches.select_folder", "-- Sélectionner un dossier --"))] + dossiers
+ str(_("pages.fiches.choose_category",)),
+ [str(_("pages.fiches.select_folder"))] + dossiers
)
- if dossier_choisi and dossier_choisi != str(_("pages.fiches.select_folder", "-- Sélectionner un dossier --")):
+ if dossier_choisi and dossier_choisi != str(_("pages.fiches.select_folder")):
fiches = arbo.get(dossier_choisi, [])
noms_fiches = [f['nom'] for f in fiches]
fiche_choisie = st.selectbox(
- str(_("pages.fiches.choose_file", "Choisissez une fiche")),
- [str(_("pages.fiches.select_file", "-- Sélectionner une fiche --"))] + noms_fiches
+ str(_("pages.fiches.choose_file")),
+ [str(_("pages.fiches.select_file"))] + noms_fiches
)
- if fiche_choisie and fiche_choisie != str(_("pages.fiches.select_file", "-- Sélectionner une fiche --")):
+ if fiche_choisie and fiche_choisie != str(_("pages.fiches.select_file")):
fiche_info = next((f for f in fiches if f["nom"] == fiche_choisie), None)
if fiche_info:
try:
@@ -101,17 +87,17 @@ def interface_fiches():
if os.path.exists(pdf_path):
with open(pdf_path, "rb") as pdf_file:
st.download_button(
- label=str(_("pages.fiches.download_pdf", "Télécharger cette fiche en PDF")),
+ label=str(_("pages.fiches.download_pdf")),
data=pdf_file,
file_name=pdf_name,
mime="application/pdf",
- help=str(_("pages.fiches.download_pdf", "Télécharger cette fiche en PDF")),
+ help=str(_("pages.fiches.download_pdf")),
key="telecharger_fiche_pdf"
)
else:
- st.warning(str(_("pages.fiches.pdf_unavailable", "Le fichier PDF de cette fiche n'est pas disponible.")))
+ st.warning(str(_("pages.fiches.pdf_unavailable")))
- st.markdown(f"## {str(_('pages.fiches.ticket_management', 'Gestion des tickets pour cette fiche'))}")
+ st.markdown(f"## {str(_('pages.fiches.ticket_management'))}")
afficher_tickets_par_fiche(rechercher_tickets_gitea(fiche_choisie))
formulaire_creation_ticket_dynamique(fiche_choisie)
diff --git a/app/fiches/utils/tickets/core.py b/app/fiches/utils/tickets/core.py
index 26f2b69..f48b3db 100644
--- a/app/fiches/utils/tickets/core.py
+++ b/app/fiches/utils/tickets/core.py
@@ -17,7 +17,7 @@ def gitea_request(method, url, **kwargs):
response.raise_for_status()
return response
except requests.RequestException as e:
- st.error(f"{str(_('errors.gitea_error', 'Erreur Gitea'))} ({method.upper()}): {e}")
+ st.error(f"{str(_('errors.gitea_error'))} ({method.upper()}): {e}")
return None
@@ -39,9 +39,9 @@ def charger_fiches_et_labels():
"item": item.strip()
}
except FileNotFoundError:
- st.error(f"❌ {str(_('errors.file_not_found', 'Le fichier'))} {chemin_csv} {str(_('errors.is_missing', 'est introuvable.'))}")
+ st.error(f"❌ {str(_('errors.file_not_found'))} {chemin_csv} {str(_('errors.is_missing'))}")
except Exception as e:
- st.error(f"❌ {str(_('errors.file_loading', 'Erreur lors du chargement des fiches :'))} {str(e)}")
+ st.error(f"❌ {str(_('errors.file_loading'))} {str(e)}")
return dictionnaire_fiches
@@ -57,7 +57,7 @@ def rechercher_tickets_gitea(fiche_selectionnee):
try:
issues = reponse.json()
except Exception as e:
- st.error(f"{str(_('errors.json_decode', 'Erreur de décodage JSON :'))} {e}")
+ st.error(f"{str(_('errors.json_decode'))} {e}")
return []
correspondances = charger_fiches_et_labels()
@@ -87,7 +87,7 @@ def get_labels_existants():
try:
return {label['name']: label['id'] for label in reponse.json()}
except Exception as e:
- st.error(f"{str(_('errors.label_parsing', 'Erreur de parsing des labels :'))} {e}")
+ st.error(f"{str(_('errors.label_parsing'))} {e}")
return {}
@@ -114,6 +114,6 @@ def creer_ticket_gitea(titre, corps, labels):
issue_url = reponse.json().get("html_url", "")
if issue_url:
- st.success(f"{str(_('pages.fiches.tickets.created_success', 'Ticket créé !'))} [Voir le ticket]({issue_url})")
+ st.success(f"{str(_('pages.fiches.tickets.created_success'))} [Voir le ticket]({issue_url})")
else:
- st.success(str(_('pages.fiches.tickets.created', 'Ticket créé avec succès.')))
+ st.success(str(_('pages.fiches.tickets.created')))
diff --git a/app/fiches/utils/tickets/creation.py b/app/fiches/utils/tickets/creation.py
index 0035a27..dd3ddb4 100644
--- a/app/fiches/utils/tickets/creation.py
+++ b/app/fiches/utils/tickets/creation.py
@@ -14,7 +14,7 @@ def parser_modele_ticket(contenu_modele):
sections = {}
lignes = contenu_modele.splitlines()
titre_courant, contenu = None, []
-
+
for ligne in lignes:
if ligne.startswith("## "):
if titre_courant:
@@ -22,10 +22,10 @@ def parser_modele_ticket(contenu_modele):
titre_courant, contenu = ligne[3:].strip(), []
elif titre_courant:
contenu.append(ligne)
-
+
if titre_courant:
sections[titre_courant] = "\n".join(contenu).strip()
-
+
return sections
@@ -34,47 +34,47 @@ def generer_labels(fiche_selectionnee):
labels, selected_ops = [], []
correspondances = charger_fiches_et_labels()
cible = correspondances.get(fiche_selectionnee)
-
+
if cible:
if len(cible["operations"]) == 1:
labels.append(cible["operations"][0])
elif len(cible["operations"]) > 1:
- selected_ops = st.multiselect(str(_("pages.fiches.tickets.contribution_type", "Labels opération à associer")),
- cible["operations"],
+ selected_ops = st.multiselect(str(_("pages.fiches.tickets.contribution_type")),
+ cible["operations"],
default=cible["operations"])
-
+
return labels, selected_ops, cible
def creer_champs_formulaire(sections, fiche_selectionnee):
"""Crée les champs du formulaire basés sur les sections."""
reponses = {}
-
+
for section, aide in sections.items():
if "Type de contribution" in section:
options = sorted(set(re.findall(r"- \[.\] (.+)", aide)))
- if str(_("pages.fiches.tickets.other", "Autre")) not in options:
- options.append(str(_("pages.fiches.tickets.other", "Autre")))
- choix = st.radio(str(_("pages.fiches.tickets.contribution_type", "Type de contribution")), options)
- reponses[section] = st.text_input(str(_("pages.fiches.tickets.specify", "Précisez")), "") if choix == str(_("pages.fiches.tickets.other", "Autre")) else choix
+ if str(_("pages.fiches.tickets.other")) not in options:
+ options.append(str(_("pages.fiches.tickets.other")))
+ choix = st.radio(str(_("pages.fiches.tickets.contribution_type")), options)
+ reponses[section] = st.text_input(str(_("pages.fiches.tickets.specify")), "") if choix == str(_("pages.fiches.tickets.other")) else choix
elif "Fiche concernée" in section:
url_fiche = f"https://fabnum-git.peccini.fr/FabNum/Fiches/src/branch/{ENV}/Documents/{fiche_selectionnee.replace(' ', '%20')}"
reponses[section] = url_fiche
- st.text_input(str(_("pages.fiches.tickets.concerned_card", "Fiche concernée")), value=url_fiche, disabled=True)
+ st.text_input(str(_("pages.fiches.tickets.concerned_card")), value=url_fiche, disabled=True)
elif "Sujet de la proposition" in section:
reponses[section] = st.text_input(section, help=aide)
else:
reponses[section] = st.text_area(section, help=aide)
-
+
return reponses
def afficher_controles_formulaire():
"""Affiche les boutons de contrôle du formulaire."""
col1, col2 = st.columns(2)
- if col1.button(str(_("pages.fiches.tickets.preview", "Prévisualiser le ticket"))):
+ if col1.button(str(_("pages.fiches.tickets.preview"))):
st.session_state.previsualiser = True
- if col2.button(str(_("pages.fiches.tickets.cancel", "Annuler"))):
+ if col2.button(str(_("pages.fiches.tickets.cancel"))):
st.session_state.previsualiser = False
st.rerun()
@@ -83,8 +83,8 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible):
"""Gère la prévisualisation et la soumission du ticket."""
if not st.session_state.get("previsualiser", False):
return
-
- st.subheader(str(_("pages.fiches.tickets.preview_title", "Prévisualisation du ticket")))
+
+ st.subheader(str(_("pages.fiches.tickets.preview_title")))
for section, texte in reponses.items():
st.markdown(f"#### {section}")
st.code(texte, language="markdown")
@@ -92,9 +92,9 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible):
titre_ticket = reponses.get("Sujet de la proposition", "").strip() or "Ticket FabNum"
final_labels = nettoyer_labels(labels + selected_ops + ([cible["item"]] if cible else []))
- st.markdown(f"**{str(_('pages.fiches.tickets.summary', 'Résumé'))} :**\n- **{str(_('pages.fiches.tickets.title', 'Titre'))}** : `{titre_ticket}`\n- **{str(_('pages.fiches.tickets.labels', 'Labels'))}** : `{', '.join(final_labels)}`")
+ st.markdown(f"**{str(_('pages.fiches.tickets.summary'))} :**\n- **{str(_('pages.fiches.tickets.title'))}** : `{titre_ticket}`\n- **{str(_('pages.fiches.tickets.labels'))}** : `{', '.join(final_labels)}`")
- if st.button(str(_("pages.fiches.tickets.confirm", "Confirmer la création du ticket"))):
+ if st.button(str(_("pages.fiches.tickets.confirm"))):
labels_existants = get_labels_existants()
labels_ids = [labels_existants[l] for l in final_labels if l in labels_existants]
if "Backlog" in labels_existants:
@@ -104,23 +104,23 @@ def gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible):
creer_ticket_gitea(titre_ticket, corps, labels_ids)
st.session_state.previsualiser = False
- st.success(str(_("pages.fiches.tickets.created", "Ticket créé et formulaire vidé.")))
+ st.success(str(_("pages.fiches.tickets.created")))
def formulaire_creation_ticket_dynamique(fiche_selectionnee):
"""Fonction principale pour le formulaire de création de ticket."""
- with st.expander(str(_("pages.fiches.tickets.create_new", "Créer un nouveau ticket lié à cette fiche")), expanded=False):
+ with st.expander(str(_("pages.fiches.tickets.create_new")), expanded=False):
# Chargement et vérification du modèle
contenu_modele = charger_modele_ticket()
if not contenu_modele:
- st.error(str(_("pages.fiches.tickets.model_load_error", "Impossible de charger le modèle de ticket.")))
+ st.error(str(_("pages.fiches.tickets.model_load_error")))
return
-
+
# Traitement du modèle et génération du formulaire
sections = parser_modele_ticket(contenu_modele)
labels, selected_ops, cible = generer_labels(fiche_selectionnee)
reponses = creer_champs_formulaire(sections, fiche_selectionnee)
-
+
# Gestion des contrôles et de la prévisualisation
afficher_controles_formulaire()
gerer_previsualisation_et_soumission(reponses, labels, selected_ops, cible)
@@ -136,5 +136,5 @@ def charger_modele_ticket():
r.raise_for_status()
return base64.b64decode(r.json().get("content", "")).decode("utf-8")
except Exception as e:
- st.error(f"{str(_('pages.fiches.tickets.model_error', 'Erreur chargement modèle :'))} {e}")
+ st.error(f"{str(_('pages.fiches.tickets.model_error'))} {e}")
return ""
diff --git a/app/fiches/utils/tickets/display.py b/app/fiches/utils/tickets/display.py
index 773921c..454b23b 100644
--- a/app/fiches/utils/tickets/display.py
+++ b/app/fiches/utils/tickets/display.py
@@ -11,22 +11,22 @@ from .core import rechercher_tickets_gitea
def extraire_statut_par_label(ticket):
labels = [label.get('name', '') for label in ticket.get('labels', [])]
- for statut in ["Backlog",
- str(_("pages.fiches.tickets.status.awaiting", "En attente de traitement")),
- str(_("pages.fiches.tickets.status.in_progress", "En cours")),
- str(_("pages.fiches.tickets.status.completed", "Terminés")),
- str(_("pages.fiches.tickets.status.rejected", "Non retenus"))]:
+ for statut in ["Backlog",
+ str(_("pages.fiches.tickets.status.awaiting")),
+ str(_("pages.fiches.tickets.status.in_progress")),
+ str(_("pages.fiches.tickets.status.completed")),
+ str(_("pages.fiches.tickets.status.rejected"))]:
if statut in labels:
return statut
- return str(_("pages.fiches.tickets.status.others", "Autres"))
+ return str(_("pages.fiches.tickets.status.others"))
def afficher_tickets_par_fiche(tickets):
if not tickets:
- st.info(str(_("pages.fiches.tickets.no_linked_tickets", "Aucun ticket lié à cette fiche.")))
+ st.info(str(_("pages.fiches.tickets.no_linked_tickets")))
return
- st.markdown(str(_("pages.fiches.tickets.associated_tickets", "**Tickets associés à cette fiche**")))
+ st.markdown(str(_("pages.fiches.tickets.associated_tickets")))
tickets_groupes = defaultdict(list)
for ticket in tickets:
statut = extraire_statut_par_label(ticket)
@@ -34,14 +34,14 @@ def afficher_tickets_par_fiche(tickets):
nb_backlogs = len(tickets_groupes["Backlog"])
if nb_backlogs:
- st.info(f"⤇ {nb_backlogs} {str(_('pages.fiches.tickets.moderation_notice', 'ticket(s) en attente de modération ne sont pas affichés.'))}")
+ st.info(f"⤇ {nb_backlogs} {str(_('pages.fiches.tickets.moderation_notice'))}")
ordre_statuts = [
- str(_("pages.fiches.tickets.status.awaiting", "En attente de traitement")),
- str(_("pages.fiches.tickets.status.in_progress", "En cours")),
- str(_("pages.fiches.tickets.status.completed", "Terminés")),
- str(_("pages.fiches.tickets.status.rejected", "Non retenus")),
- str(_("pages.fiches.tickets.status.others", "Autres"))
+ str(_("pages.fiches.tickets.status.awaiting")),
+ str(_("pages.fiches.tickets.status.in_progress")),
+ str(_("pages.fiches.tickets.status.completed")),
+ str(_("pages.fiches.tickets.status.rejected")),
+ str(_("pages.fiches.tickets.status.others"))
]
for statut in ordre_statuts:
if tickets_groupes[statut]:
@@ -61,14 +61,14 @@ def recuperer_commentaires_ticket(issue_index):
response.raise_for_status()
return response.json()
except Exception as e:
- st.error(f"{str(_('pages.fiches.tickets.comment_error', 'Erreur lors de la récupération des commentaires :'))} {e}")
+ st.error(f"{str(_('pages.fiches.tickets.comment_error'))} {e}")
return []
def afficher_carte_ticket(ticket):
- titre = ticket.get("title", str(_("pages.fiches.tickets.no_title", "Sans titre")))
+ titre = ticket.get("title", str(_("pages.fiches.tickets.no_title")))
url = ticket.get("html_url", "")
- user = ticket.get("user", {}).get("login", str(_("pages.fiches.tickets.unknown", "inconnu")))
+ user = ticket.get("user", {}).get("login", str(_("pages.fiches.tickets.unknown")))
created = ticket.get("created_at", "")
updated = ticket.get("updated_at", "")
body = ticket.get("body", "")
@@ -86,12 +86,12 @@ def afficher_carte_ticket(ticket):
return "?"
date_created_str = format_date(created)
- maj_info = f"({str(_('pages.fiches.tickets.updated', 'MAJ'))} {format_date(updated)})" if updated and updated != created else ""
+ maj_info = f"({str(_('pages.fiches.tickets.updated'))} {format_date(updated)})" if updated and updated != created else ""
commentaires = recuperer_commentaires_ticket(ticket.get("number"))
commentaires_html = ""
for commentaire in commentaires:
- auteur = html.escape(commentaire.get('user', {}).get('login', str(_("pages.fiches.tickets.unknown", "inconnu"))))
+ auteur = html.escape(commentaire.get('user', {}).get('login', str(_("pages.fiches.tickets.unknown"))))
contenu = html.escape(commentaire.get('body', ''))
date = format_date(commentaire.get('created_at', ''))
commentaires_html += f"""
@@ -105,12 +105,12 @@ def afficher_carte_ticket(ticket):
st.markdown(f"""
-
{str(_("pages.fiches.tickets.opened_by", "Ouvert par"))} {html.escape(user)} {str(_("pages.fiches.tickets.on_date", "le"))} {date_created_str} {maj_info}
-
{str(_("pages.fiches.tickets.subject_label", "Sujet"))} : {html.escape(sujet)}
-
Labels : {' • '.join(labels) if labels else str(_("pages.fiches.tickets.no_labels", "aucun"))}
+
{str(_("pages.fiches.tickets.opened_by"))} {html.escape(user)} {str(_("pages.fiches.tickets.on_date"))} {date_created_str} {maj_info}
+
{str(_("pages.fiches.tickets.subject_label"))} : {html.escape(sujet)}
+
Labels : {' • '.join(labels) if labels else str(_("pages.fiches.tickets.no_labels"))}
""", unsafe_allow_html=True)
st.markdown(body, unsafe_allow_html=False)
st.markdown("---")
- st.markdown(str(_("pages.fiches.tickets.comments", "**Commentaire(s) :**")))
- st.markdown(commentaires_html or str(_("pages.fiches.tickets.no_comments", "Aucun commentaire.")), unsafe_allow_html=True)
+ st.markdown(str(_("pages.fiches.tickets.comments")))
+ st.markdown(commentaires_html or str(_("pages.fiches.tickets.no_comments")), unsafe_allow_html=True)
diff --git a/app/personnalisation/ajout.py b/app/personnalisation/ajout.py
index 3686a0b..58e9728 100644
--- a/app/personnalisation/ajout.py
+++ b/app/personnalisation/ajout.py
@@ -2,22 +2,22 @@ import streamlit as st
from utils.translations import _
def ajouter_produit(G):
- st.markdown(f"## {str(_('pages.personnalisation.add_new_product', 'Ajouter un nouveau produit final'))}")
- new_prod = st.text_input(str(_("pages.personnalisation.new_product_name", "Nom du nouveau produit (unique)")), key="new_prod")
+ st.markdown(f"## {str(_('pages.personnalisation.add_new_product'))}")
+ new_prod = st.text_input(str(_("pages.personnalisation.new_product_name")), key="new_prod")
if new_prod:
ops_dispo = sorted([
n for n, d in G.nodes(data=True)
if d.get("niveau") == "10"
and any(G.has_edge(p, n) and G.nodes[p].get("niveau") == "0" for p in G.predecessors(n))
])
- sel_new_op = st.selectbox(str(_("pages.personnalisation.assembly_operation", "Opération d'assemblage (optionnelle)")), [str(_("pages.personnalisation.none", "-- Aucune --"))] + ops_dispo, index=0)
+ sel_new_op = st.selectbox(str(_("pages.personnalisation.assembly_operation")), [str(_("pages.personnalisation.none"))] + ops_dispo, index=0)
niveau1 = sorted([n for n, d in G.nodes(data=True) if d.get("niveau") == "1"])
- sel_comps = st.multiselect(str(_("pages.personnalisation.components_to_link", "Composants à lier")), options=niveau1)
- if st.button(str(_("pages.personnalisation.create_product", "Créer le produit"))):
+ sel_comps = st.multiselect(str(_("pages.personnalisation.components_to_link")), options=niveau1)
+ if st.button(str(_("pages.personnalisation.create_product"))):
G.add_node(new_prod, niveau="0", personnalisation="oui", label=new_prod)
- if sel_new_op != str(_("pages.personnalisation.none", "-- Aucune --")):
+ if sel_new_op != str(_("pages.personnalisation.none")):
G.add_edge(new_prod, sel_new_op)
for comp in sel_comps:
G.add_edge(new_prod, comp)
- st.success(f"{new_prod} {str(_('pages.personnalisation.added', 'ajouté'))}")
+ st.success(f"{new_prod} {str(_('pages.personnalisation.added'))}")
return G
diff --git a/app/personnalisation/import_export.py b/app/personnalisation/import_export.py
index 380fd2d..5d3e6bc 100644
--- a/app/personnalisation/import_export.py
+++ b/app/personnalisation/import_export.py
@@ -1,24 +1,25 @@
import streamlit as st
import json
+from utils.translations import get_translation as _
def importer_exporter_graph(G):
- st.markdown("## Sauvegarder ou restaurer la configuration")
- if st.button("Exporter configuration"):
+ st.markdown(f"## {_('pages.personnalisation.save_restore_config')}")
+ if st.button(str(_("pages.personnalisation.export_config"))):
nodes = [n for n, d in G.nodes(data=True) if d.get("personnalisation") == "oui"]
edges = [(u, v) for u, v in G.edges() if u in nodes]
conf = {"nodes": nodes, "edges": edges}
json_str = json.dumps(conf, ensure_ascii=False)
st.download_button(
- label="Télécharger (JSON)",
+ label=str(_("pages.personnalisation.download_json")),
data=json_str,
file_name="config_personnalisation.json",
mime="application/json"
)
- uploaded = st.file_uploader("Importer une configuration JSON (max 100 Ko)", type=["json"])
+ uploaded = st.file_uploader(str(_("pages.personnalisation.import_config")), type=["json"])
if uploaded:
if uploaded.size > 100 * 1024:
- st.error("Fichier trop volumineux (max 100 Ko).")
+ st.error(_("pages.personnalisation.file_too_large"))
else:
try:
conf = json.loads(uploaded.read().decode("utf-8"))
@@ -26,17 +27,17 @@ def importer_exporter_graph(G):
all_edges = conf.get("edges", [])
if not all_nodes:
- st.warning("Aucun produit trouvé dans le fichier.")
+ st.warning(_("pages.personnalisation.no_products_found"))
else:
- st.markdown("### Sélection des produits à restaurer")
+ st.markdown(f"### {_('pages.personnalisation.select_products_to_restore')}")
sel_nodes = st.multiselect(
- "Produits à restaurer",
+ str(_("pages.personnalisation.products_to_restore")),
options=all_nodes,
default=all_nodes,
key="restaurer_selection"
)
- if st.button("Restaurer les éléments sélectionnés", type="primary"):
+ if st.button(str(_("pages.personnalisation.restore_selected")), type="primary"):
for node in sel_nodes:
if not G.has_node(node):
G.add_node(node, niveau="0", personnalisation="oui", label=node)
@@ -45,8 +46,8 @@ def importer_exporter_graph(G):
if u in sel_nodes and v in sel_nodes + list(G.nodes()) and not G.has_edge(u, v):
G.add_edge(u, v)
- st.success("Configuration partielle restaurée avec succès.")
+ st.success(_("pages.personnalisation.config_restored"))
except Exception as e:
- st.error(f"Erreur d'import : {e}")
+ st.error(f"{_('errors.import_error')} {e}")
return G
diff --git a/app/personnalisation/interface.py b/app/personnalisation/interface.py
index 4e3573f..bcaae19 100644
--- a/app/personnalisation/interface.py
+++ b/app/personnalisation/interface.py
@@ -7,18 +7,9 @@ from .modification import modifier_produit
from .import_export import importer_exporter_graph
def interface_personnalisation(G):
- st.markdown(f"# {str(_('pages.personnalisation.title', 'Personnalisation des produits finaux'))}")
- with st.expander(str(_("pages.personnalisation.help", "Comment utiliser cet onglet ?")), expanded=False):
- st.markdown("\n".join([
- " " + line for line in _("pages.personnalisation.help_content", [
- "1. Cliquez sur « Ajouter un produit final » pour créer un nouveau produit",
- "2. Donnez un nom à votre produit",
- "3. Sélectionnez une opération d'assemblage appropriée (si pertinent)",
- "4. Choisissez les composants qui constituent votre produit dans la liste proposée",
- "5. Sauvegardez votre configuration pour une réutilisation future",
- "6. Vous pourrez par la suite modifier ou supprimer vos produits personnalisés"
- ])
- ]))
+ st.markdown(f"# {str(_('pages.personnalisation.title'))}")
+ with st.expander(str(_("pages.personnalisation.help")), expanded=False):
+ st.markdown("\n".join(_("pages.personnalisation.help_content")))
st.markdown("---")
G = ajouter_produit(G)
diff --git a/app/personnalisation/modification.py b/app/personnalisation/modification.py
index 35bc202..e948a6c 100644
--- a/app/personnalisation/modification.py
+++ b/app/personnalisation/modification.py
@@ -8,7 +8,7 @@ def get_produits_personnalises(G):
def supprimer_produit(G, prod):
"""Supprime un produit du graphe."""
G.remove_node(prod)
- st.success(f"{prod} {str(_('pages.personnalisation.deleted', 'supprimé'))}")
+ st.success(f"{prod} {str(_('pages.personnalisation.deleted'))}")
st.session_state.pop("prod_sel", None)
return G
@@ -51,37 +51,37 @@ def mettre_a_jour_composants(G, prod, linked, nouveaux):
return G
def modifier_produit(G):
- st.markdown(f"## {str(_('pages.personnalisation.modify_product', 'Modifier un produit final ajouté'))}")
-
+ st.markdown(f"## {str(_('pages.personnalisation.modify_product'))}")
+
# Sélection du produit à modifier
produits0 = get_produits_personnalises(G)
- sel_display = st.multiselect(str(_("pages.personnalisation.products_to_modify", "Produits à modifier")), options=produits0)
+ sel_display = st.multiselect(str(_("pages.personnalisation.products_to_modify")), options=produits0)
if not sel_display:
return G
# Obtention du produit sélectionné
prod = sel_display[0]
-
+
# Suppression du produit si demandé
- if st.button(f"{str(_('pages.personnalisation.delete', 'Supprimer'))} {prod}"):
+ if st.button(f"{str(_('pages.personnalisation.delete'))} {prod}"):
return supprimer_produit(G, prod)
# Gestion des opérations d'assemblage
ops_dispo = get_operations_disponibles(G)
curr_ops = get_operations_actuelles(G, prod)
default_idx = ops_dispo.index(curr_ops[0]) + 1 if curr_ops and curr_ops[0] in ops_dispo else 0
- sel_op = st.selectbox(str(_("pages.personnalisation.linked_assembly_operation", "Opération d'assemblage liée")), [str(_("pages.personnalisation.none", "-- Aucune --"))] + ops_dispo, index=default_idx)
+ sel_op = st.selectbox(str(_("pages.personnalisation.linked_assembly_operation")), [str(_("pages.personnalisation.none"))] + ops_dispo, index=default_idx)
# Gestion des composants
niveau1 = get_composants_niveau1(G)
linked = get_composants_lies(G, prod)
- nouveaux = st.multiselect(f"{str(_('pages.personnalisation.components_linked_to', 'Composants liés à'))} {prod}", options=niveau1, default=linked)
+ nouveaux = st.multiselect(f"{str(_('pages.personnalisation.components_linked_to'))} {prod}", options=niveau1, default=linked)
# Mise à jour des liens si demandé
- if st.button(f"{str(_('pages.personnalisation.update', 'Mettre à jour'))} {prod}"):
+ if st.button(f"{str(_('pages.personnalisation.update'))} {prod}"):
G = mettre_a_jour_operations(G, prod, curr_ops, sel_op)
G = mettre_a_jour_composants(G, prod, linked, nouveaux)
- st.success(f"{prod} {str(_('pages.personnalisation.updated', 'mis à jour'))}")
-
+ st.success(f"{prod} {str(_('pages.personnalisation.updated'))}")
+
return G
diff --git a/app/visualisations/graphes.py b/app/visualisations/graphes.py
index 9c30dd4..e5adddc 100644
--- a/app/visualisations/graphes.py
+++ b/app/visualisations/graphes.py
@@ -9,18 +9,18 @@ from utils.translations import _
def afficher_graphique_altair(df):
# Définir les catégories originales (en français) et leur ordre
categories_fr = ["Assemblage", "Fabrication", "Traitement", "Extraction"]
-
+
# Créer un dictionnaire de mappage entre les catégories originales et leurs traductions
mappage_categories = {
- "Assemblage": str(_("pages.visualisations.categories.assembly", "Assemblage")),
- "Fabrication": str(_("pages.visualisations.categories.manufacturing", "Fabrication")),
- "Traitement": str(_("pages.visualisations.categories.processing", "Traitement")),
- "Extraction": str(_("pages.visualisations.categories.extraction", "Extraction"))
+ "Assemblage": str(_("pages.visualisations.categories.assembly")),
+ "Fabrication": str(_("pages.visualisations.categories.manufacturing")),
+ "Traitement": str(_("pages.visualisations.categories.processing")),
+ "Extraction": str(_("pages.visualisations.categories.extraction"))
}
-
+
# Filtrer les catégories qui existent dans les données
categories_fr_filtrees = [cat for cat in categories_fr if cat in df['categorie'].unique()]
-
+
# Parcourir les catégories dans l'ordre défini
for cat_fr in categories_fr_filtrees:
# Obtenir le nom traduit de la catégorie pour l'affichage
@@ -53,8 +53,8 @@ def afficher_graphique_altair(df):
df_cat['ihh_acteurs_text'] = df_cat['ihh_acteurs'] + 0.5
base = alt.Chart(df_cat).encode(
- x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries", "IHH Pays (%)"))),
- y=alt.Y('ihh_acteurs:Q', title=str(_("pages.visualisations.axis_titles.ihh_actors", "IHH Acteurs (%)"))),
+ x=alt.X('ihh_pays:Q', title=str(_("pages.visualisations.axis_titles.ihh_countries"))),
+ y=alt.Y('ihh_acteurs:Q', title=str(_("pages.visualisations.axis_titles.ihh_actors"))),
size=alt.Size('criticite_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None),
color=alt.Color('criticite_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred']))
)
@@ -83,7 +83,7 @@ def afficher_graphique_altair(df):
chart = (points + lines + labels + hline_15 + hline_25 + hline_100 + vline_15 + vline_25 + vline_100).properties(
width=500,
height=400,
- title=str(_("pages.visualisations.chart_titles.concentration_criticality", "Concentration et criticité – {0}")).format(cat_traduit)
+ title=str(_("pages.visualisations.chart_titles.concentration_criticality")).format(cat_traduit)
).interactive()
st.altair_chart(chart, use_container_width=True)
@@ -91,7 +91,7 @@ def afficher_graphique_altair(df):
def creer_graphes(donnees):
if not donnees:
- st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à afficher.")))
+ st.warning(str(_("pages.visualisations.no_data")))
return
try:
@@ -122,8 +122,8 @@ def creer_graphes(donnees):
df['ihh_reserves_text'] = df['ihh_reserves'] + 0.5
base = alt.Chart(df).encode(
- x=alt.X('ihh_extraction:Q', title=str(_("pages.visualisations.axis_titles.ihh_extraction", "IHH Extraction (%)"))),
- y=alt.Y('ihh_reserves:Q', title=str(_("pages.visualisations.axis_titles.ihh_reserves", "IHH Réserves (%)"))),
+ x=alt.X('ihh_extraction:Q', title=str(_("pages.visualisations.axis_titles.ihh_extraction"))),
+ y=alt.Y('ihh_reserves:Q', title=str(_("pages.visualisations.axis_titles.ihh_reserves"))),
size=alt.Size('ivc_cat:Q', scale=alt.Scale(domain=[1, 2, 3], range=[50, 500, 1000]), legend=None),
color=alt.Color('ivc_cat:N', scale=alt.Scale(domain=[1, 2, 3], range=['darkgreen', 'orange', 'darkred'])),
tooltip=['nom:N', 'ivc:Q', 'ihh_extraction:Q', 'ihh_reserves:Q']
@@ -153,13 +153,13 @@ def creer_graphes(donnees):
chart = (points + lines + labels + hline_15 + hline_25 + hline_100 + vline_15 + vline_25 + vline_100).properties(
width=600,
height=500,
- title=str(_("pages.visualisations.chart_titles.concentration_resources", "Concentration des ressources critiques vs vulnérabilité IVC"))
+ title=str(_("pages.visualisations.chart_titles.concentration_resources"))
).interactive()
st.altair_chart(chart, use_container_width=True)
except Exception as e:
- st.error(f"{str(_('errors.graph_creation_error', 'Erreur lors de la création du graphique :'))} {e}")
+ st.error(f"{str(_('errors.graph_creation_error'))} {e}")
def lancer_visualisation_ihh_criticite(graph):
@@ -173,11 +173,11 @@ def lancer_visualisation_ihh_criticite(graph):
df = recuperer_donnees(graph, noeuds)
if df.empty:
- st.warning(str(_("pages.visualisations.no_data", "Aucune donnée à visualiser.")))
+ st.warning(str(_("pages.visualisations.no_data")))
else:
afficher_graphique_altair(df)
except Exception as e:
- st.error(f"{str(_('errors.ihh_criticality_error', 'Erreur dans la visualisation IHH vs Criticité :'))} {e}")
+ st.error(f"{str(_('errors.ihh_criticality_error'))} {e}")
def lancer_visualisation_ihh_ivc(graph):
@@ -192,4 +192,4 @@ def lancer_visualisation_ihh_ivc(graph):
data = recuperer_donnees_2(graph, noeuds_niveau_2)
creer_graphes(data)
except Exception as e:
- st.error(f"{str(_('errors.ihh_ivc_error', 'Erreur dans la visualisation IHH vs IVC :'))} {e}")
+ st.error(f"{str(_('errors.ihh_ivc_error'))} {e}")
diff --git a/app/visualisations/interface.py b/app/visualisations/interface.py
index e15c698..49d9db8 100644
--- a/app/visualisations/interface.py
+++ b/app/visualisations/interface.py
@@ -8,39 +8,28 @@ from .graphes import (
def interface_visualisations(G_temp, G_temp_ivc):
- st.markdown(f"# {str(_('pages.visualisations.title', 'Visualisations'))}")
- with st.expander(str(_("pages.visualisations.help", "Comment utiliser cet onglet ?")), expanded=False):
- st.markdown("\n".join(_("pages.visualisations.help_content", [
- "1. Explorez les graphiques présentant l'Indice de Herfindahl-Hirschmann (IHH)",
- "2. Analysez sa relation avec la criticité moyenne des minerais ou leur Indice de Vulnérabilité Concurrentielle (IVC)",
- "3. Zoomer dans les graphes pour mieux découvrir les informations",
- "",
- "Il est important de se rappeler que l'IHH a deux seuils :",
- "* en-dessous de 15, la concentration est considérée comme étant faible",
- "* au-dessus de 25, elle est considérée comme étant forte",
- "",
- "Ainsi plus le positionnement d'un point est en haut à droite des graphiques, plus les risques sont élevés.",
- "Les graphiques présentent 2 droites horizontales et vetrticales pour matérialiser ces seuils."
- ])))
+ st.markdown(f"# {str(_('pages.visualisations.title'))}")
+ with st.expander(str(_("pages.visualisations.help")), expanded=False):
+ st.markdown("\n".join(_("pages.visualisations.help_content")))
st.markdown("---")
- st.markdown(f"""## {str(_("pages.visualisations.ihh_criticality", "Indice de Herfindahl-Hirschmann - IHH vs Criticité"))}
+ st.markdown(f"""## {str(_("pages.visualisations.ihh_criticality"))}
-{str(_("pages.visualisations.ihh_criticality_desc", "La taille des points donne l'indication de la criticité de substituabilité du minerai."))}
+{str(_("pages.visualisations.ihh_criticality_desc"))}
""")
- if st.button(str(_("buttons.run", "Lancer")), key="btn_ihh_criticite"):
+ if st.button(str(_("buttons.run")), key="btn_ihh_criticite"):
try:
lancer_visualisation_ihh_criticite(G_temp)
except Exception as e:
- st.error(f"{str(_('errors.ihh_criticality_error', 'Erreur dans la visualisation IHH vs Criticité :'))} {e}")
+ st.error(f"{str(_('errors.ihh_criticality_error'))} {e}")
- st.markdown(f"""## {str(_("pages.visualisations.ihh_ivc", "Indice de Herfindahl-Hirschmann - IHH vs IVC"))}
+ st.markdown(f"""## {str(_("pages.visualisations.ihh_ivc"))}
-{str(_("pages.visualisations.ihh_ivc_desc", "La taille des points donne l'indication de la criticité concurrentielle du minerai."))}
+{str(_("pages.visualisations.ihh_ivc_desc"))}
""")
- if st.button(str(_("buttons.run", "Lancer")), key="btn_ihh_ivc"):
+ if st.button(str(_("buttons.run")), key="btn_ihh_ivc"):
try:
lancer_visualisation_ihh_ivc(G_temp_ivc)
except Exception as e:
- st.error(f"{str(_('errors.ihh_ivc_error', 'Erreur dans la visualisation IHH vs IVC :'))} {e}")
+ st.error(f"{str(_('errors.ihh_ivc_error'))} {e}")
diff --git a/assets/locales/en copy.json b/assets/locales/en copy.json
new file mode 100644
index 0000000..86a5128
--- /dev/null
+++ b/assets/locales/en copy.json
@@ -0,0 +1,276 @@
+{
+ "app": {
+ "title": "Fabnum – Chain Analysis",
+ "description": "Ecosystem exploration and vulnerability identification.",
+ "dev_mode": "You are in the development environment."
+ },
+ "header": {
+ "title": "FabNum - Digital Manufacturing Chain",
+ "subtitle": "Ecosystem exploration and vulnerability identification."
+ },
+ "footer": {
+ "copyright": "Fabnum © 2025",
+ "contact": "Contact",
+ "license": "License",
+ "license_text": "CC BY-NC-ND",
+ "eco_note": "🌱 CO₂ calculations via",
+ "eco_provider": "The Green Web Foundation",
+ "powered_by": "🚀 Powered by",
+ "powered_by_name": "Streamlit"
+ },
+ "sidebar": {
+ "menu": "Main Menu",
+ "navigation": "Main Navigation",
+ "theme": "Theme",
+ "theme_light": "Light",
+ "theme_dark": "Dark",
+ "theme_instructions_only": "Theme changes can only be made from the Instructions tab.",
+ "impact": "Environmental Impact",
+ "loading": "Loading..."
+ },
+ "auth": {
+ "title": "Authentication",
+ "username": "Username_token",
+ "token": "Gitea Personal Access Token",
+ "login": "Login",
+ "logout": "Logout",
+ "logged_as": "Logged in as",
+ "error": "❌ Access denied.",
+ "gitea_error": "❌ Unable to verify user with Gitea.",
+ "success": "Successfully logged out."
+ },
+ "navigation": {
+ "instructions": "Instructions",
+ "personnalisation": "Customization",
+ "analyse": "Analysis",
+ "visualisations": "Visualizations",
+ "fiches": "Cards"
+ },
+ "pages": {
+ "instructions": {
+ "title": "Instructions"
+ },
+ "personnalisation": {
+ "title": "Final Product Customization",
+ "help": "How to use this tab?",
+ "help_content": [
+ "1. Click on \"Add a final product\" to create a new product",
+ "2. Give your product a name",
+ "3. Select an appropriate assembly operation (if relevant)",
+ "4. Choose the components that make up your product from the list provided",
+ "5. Save your configuration for future reuse",
+ "6. You will be able to modify or delete your custom products later"
+ ],
+ "add_new_product": "Add a new final product",
+ "new_product_name": "New product name (unique)",
+ "assembly_operation": "Assembly operation (optional)",
+ "none": "-- None --",
+ "components_to_link": "Components to link",
+ "create_product": "Create product",
+ "added": "added",
+ "modify_product": "Modify an added final product",
+ "products_to_modify": "Products to modify",
+ "delete": "Delete",
+ "linked_assembly_operation": "Linked assembly operation",
+ "components_linked_to": "Components linked to",
+ "update": "Update",
+ "updated": "updated",
+ "deleted": "deleted",
+ "save_restore_config": "Save or restore configuration",
+ "export_config": "Export configuration",
+ "download_json": "Download (JSON)",
+ "import_config": "Import a JSON configuration (max 100 KB)",
+ "file_too_large": "File too large (max 100 KB).",
+ "no_products_found": "No products found in the file.",
+ "select_products_to_restore": "Select products to restore",
+ "products_to_restore": "Products to restore",
+ "restore_selected": "Restore selected items",
+ "config_restored": "Partial configuration successfully restored.",
+ "import_error": "Import error:"
+ },
+ "analyse": {
+ "title": "Graph Analysis",
+ "help": "How to use this tab?",
+ "help_content": [
+ "1. Select the starting level (final product, component, or mineral)",
+ "2. Choose the desired destination level",
+ "3. Refine your selection by specifying either one or more specific minerals to target or specific items at each level (optional)",
+ "4. Define the analysis criteria by selecting the relevant vulnerability indices",
+ "5. Choose the index combination mode (AND/OR) according to your analysis needs",
+ "6. Explore the generated graph using zoom and panning controls; you can switch to full screen mode for the graph"
+ ],
+ "selection_nodes": "Selection of start and end nodes",
+ "select_level": "-- Select a level --",
+ "start_level": "Start level",
+ "end_level": "End level",
+ "select_minerals": "Select one or more minerals",
+ "filter_by_minerals": "Filter by minerals (optional)",
+ "fine_selection": "Fine selection of items",
+ "filter_start_nodes": "Filter by start nodes (optional)",
+ "filter_end_nodes": "Filter by end nodes (optional)",
+ "vulnerability_filters": "Selection of filters to identify vulnerabilities",
+ "filter_ics": "Filter paths containing at least one critical mineral for a component (ICS > 66%)",
+ "filter_ivc": "Filter paths containing at least one critical mineral in relation to sectoral competition (IVC > 30)",
+ "filter_ihh": "Filter paths containing at least one critical operation in relation to geographical or industrial concentration (IHH countries or actors > 25)",
+ "apply_ihh_filter": "Apply IHH filter on:",
+ "countries": "Countries",
+ "actors": "Actors",
+ "filter_isg": "Filter paths containing an unstable country (ISG ≥ 60)",
+ "filter_logic": "Filter logic",
+ "or": "OR",
+ "and": "AND",
+ "run_analysis": "Run analysis",
+ "sankey": {
+ "no_paths": "No paths found for the specified criteria.",
+ "no_matching_paths": "No paths match the criteria.",
+ "filtered_hierarchy": "Hierarchy filtered by levels and nodes",
+ "download_dot": "Download filtered DOT file",
+ "relation": "Relation"
+ }
+ },
+ "visualisations": {
+ "title": "Visualizations",
+ "help": "How to use this tab?",
+ "help_content": [
+ "1. Explore the graphs presenting the Herfindahl-Hirschmann Index (IHH)",
+ "2. Analyze its relationship with the average criticality of minerals or their Competitive Vulnerability Index (IVC)",
+ "3. Zoom in on the graphs to better discover the information",
+ "",
+ "It is important to remember that the IHH has two thresholds:",
+ "* below 15, concentration is considered to be low",
+ "* above 25, it is considered to be high",
+ "",
+ "Thus, the higher a point is positioned in the top right of the graphs, the higher the risks.",
+ "The graphs present 2 horizontal and vertical lines to mark these thresholds."
+ ],
+ "ihh_criticality": "Herfindahl-Hirschmann Index - IHH vs Criticality",
+ "ihh_criticality_desc": "The size of the points indicates the substitutability criticality of the mineral.",
+ "ihh_ivc": "Herfindahl-Hirschmann Index - IHH vs IVC",
+ "ihh_ivc_desc": "The size of the points indicates the competitive criticality of the mineral.",
+ "launch": "Launch",
+ "no_data": "No data to display.",
+ "categories": {
+ "assembly": "Assembly",
+ "manufacturing": "Manufacturing",
+ "processing": "Processing",
+ "extraction": "Extraction"
+ },
+ "axis_titles": {
+ "ihh_countries": "IHH Countries (%)",
+ "ihh_actors": "IHH Actors (%)",
+ "ihh_extraction": "IHH Extraction (%)",
+ "ihh_reserves": "IHH Reserves (%)"
+ },
+ "chart_titles": {
+ "concentration_criticality": "Concentration and Criticality – {0}",
+ "concentration_resources": "Concentration of Critical Resources vs IVC Vulnerability"
+ }
+ },
+ "fiches": {
+ "title": "Card Discovery",
+ "help": "How to use this tab?",
+ "help_content": [
+ "1. Browse the list of available cards by category",
+ "2. Select a card to display its full content",
+ "3. Consult detailed data, graphs, and additional analyses",
+ "4. Use this information to deepen your understanding of the identified vulnerabilities",
+ "",
+ "The categories are as follows:",
+ "* Assembly: operation of assembling final products from components",
+ "* Related: various operations necessary to manufacture digital technology, but not directly entering its composition",
+ "* Criticalities: indices used to identify and evaluate vulnerabilities",
+ "* Manufacturing: operation of manufacturing components from minerals",
+ "* Mineral: description and operations of extraction and processing of minerals"
+ ],
+ "no_files": "No cards available at the moment.",
+ "choose_category": "Choose a card category",
+ "select_folder": "-- Select a folder --",
+ "choose_file": "Choose a card",
+ "select_file": "-- Select a card --",
+ "loading_error": "Error loading the card:",
+ "download_pdf": "Download this card as PDF",
+ "pdf_unavailable": "The PDF file for this card is not available.",
+ "ticket_management": "Ticket management for this card",
+ "tickets": {
+ "create_new": "Create a new ticket linked to this card",
+ "model_load_error": "Unable to load the ticket template.",
+ "contribution_type": "Contribution type",
+ "specify": "Specify",
+ "other": "Other",
+ "concerned_card": "Concerned card",
+ "subject": "Subject of the proposal",
+ "preview": "Preview ticket",
+ "cancel": "Cancel",
+ "preview_title": "Ticket preview",
+ "summary": "Summary",
+ "title": "Title",
+ "labels": "Labels",
+ "confirm": "Confirm ticket creation",
+ "created": "Ticket created and form cleared.",
+ "model_error": "Template loading error:",
+ "no_linked_tickets": "No tickets linked to this card.",
+ "associated_tickets": "Tickets associated with this card",
+ "moderation_notice": "ticket(s) awaiting moderation are not displayed.",
+ "status": {
+ "awaiting": "Awaiting processing",
+ "in_progress": "In progress",
+ "completed": "Completed",
+ "rejected": "Rejected",
+ "others": "Others"
+ },
+ "no_title": "No title",
+ "unknown": "unknown",
+ "subject_label": "Subject",
+ "no_labels": "none",
+ "comments": "Comment(s):",
+ "no_comments": "No comments.",
+ "comment_error": "Error retrieving comments:",
+ "opened_by": "Opened by",
+ "on_date": "on",
+ "updated": "UPDATED"
+ }
+ }
+ },
+ "node_levels": {
+ "0": "Final product",
+ "1": "Component",
+ "2": "Mineral",
+ "10": "Operation",
+ "11": "Operation country",
+ "12": "Operation actor",
+ "99": "Geographic country"
+ },
+ "errors": {
+ "log_read_error": "Log reading error:",
+ "graph_preview_error": "Graph preview error:",
+ "graph_creation_error": "Error creating the graph:",
+ "ihh_criticality_error": "Error in IHH vs Criticality visualization:",
+ "ihh_ivc_error": "Error in IHH vs IVC visualization:",
+ "comment_fetch_error": "Error retrieving comments:",
+ "template_load_error": "Template loading error:",
+ "import_error": "Import error:"
+ },
+ "buttons": {
+ "download": "Download",
+ "run": "Run",
+ "save": "Save",
+ "cancel": "Cancel",
+ "confirm": "Confirm",
+ "filter": "Filter",
+ "search": "Search",
+ "create": "Create",
+ "update": "Update",
+ "delete": "Delete",
+ "preview": "Preview",
+ "export": "Export",
+ "import": "Import",
+ "restore": "Restore",
+ "browse_files": "Browse files"
+ },
+ "ui": {
+ "file_uploader": {
+ "drag_drop_here": "Drag and drop file here",
+ "size_limit": "100 KB limit per file • JSON"
+ }
+ }
+}
diff --git a/components/connexion.py b/components/connexion.py
index cb6e392..a5edc98 100644
--- a/components/connexion.py
+++ b/components/connexion.py
@@ -20,7 +20,7 @@ def initialiser_logger():
def connexion():
if "logged_in" not in st.session_state or not st.session_state.logged_in:
- auth_title = str(_("auth.title", "Authentification"))
+ auth_title = str(_("auth.title"))
st.html(f"""
@@ -42,9 +42,9 @@ def connexion():
with st.form("auth_form"):
# Ajout d'un champ identifiant fictif pour activer l'autocomplétion navigateur
# et permettre de stocker le token comme un mot de passe par le navigateur
- identifiant = st.text_input(str(_("auth.username", "Identifiant_token")), value="fabnum-connexion", key="nom_utilisateur")
- token = st.text_input(str(_("auth.token", "Token d'accès personnel Gitea")), type="password")
- submitted = st.form_submit_button(str(_("auth.login", "Se connecter")))
+ identifiant = st.text_input(str(_("auth.username")), value="fabnum-connexion", key="nom_utilisateur")
+ token = st.text_input(str(_("auth.token")), type="password")
+ submitted = st.form_submit_button(str(_("auth.login")))
if submitted and token:
erreur = True
@@ -78,11 +78,11 @@ def connexion():
st.rerun()
except requests.RequestException:
- st.error(str(_("auth.gitea_error", "❌ Impossible de vérifier l'utilisateur auprès de Gitea.")))
+ st.error(str(_("auth.gitea_error")))
if erreur:
logger.warning(f"Accès refusé pour tentative avec token depuis IP {ip}")
- st.error(str(_("auth.error", "❌ Accès refusé.")))
+ st.error(str(_("auth.error")))
st.html("""
@@ -92,22 +92,22 @@ def connexion():
def bouton_deconnexion():
if st.session_state.get("logged_in", False):
- auth_title = str(_("auth.title", "Authentification"))
+ auth_title = str(_("auth.title"))
st.html(f"""
{auth_title}
""")
- st.sidebar.markdown(f"{str(_('auth.logged_as', 'Connecté en tant que'))} `{st.session_state.username}`")
- if st.sidebar.button(str(_("auth.logout", "Se déconnecter"))):
+ st.sidebar.markdown(f"{str(_('auth.logged_as'))} `{st.session_state.username}`")
+ if st.sidebar.button(str(_("auth.logout"))):
st.session_state.logged_in = False
st.session_state.username = ""
st.session_state.token = ""
- st.success(str(_("auth.success", "Déconnecté avec succès.")))
+ st.success(str(_("auth.success")))
st.rerun()
st.html("""
- """)
\ No newline at end of file
+ """)
diff --git a/components/footer.py b/components/footer.py
index bc84bb8..3e9f4a1 100644
--- a/components/footer.py
+++ b/components/footer.py
@@ -11,11 +11,11 @@ def afficher_pied_de_page():
diff --git a/components/header.py b/components/header.py
index 560b92b..643a87a 100644
--- a/components/header.py
+++ b/components/header.py
@@ -7,13 +7,13 @@ def afficher_entete():
header = f"""
diff --git a/components/sidebar.py b/components/sidebar.py
index 9f43fbb..63e88bb 100644
--- a/components/sidebar.py
+++ b/components/sidebar.py
@@ -7,22 +7,22 @@ from utils.translations import _
def afficher_menu():
with st.sidebar:
st.markdown(f"""
-