From f4a28db6f7413972cbfd1684da3c49db472cf4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phan?= Date: Wed, 14 May 2025 12:39:24 +0200 Subject: [PATCH] Modification internationalisation --- app/analyse/interface.py | 53 +++-- app/analyse/sankey.py | 34 +++- app/fiches/interface.py | 42 ++-- app/fiches/utils/tickets/core.py | 14 +- app/fiches/utils/tickets/creation.py | 52 ++--- app/fiches/utils/tickets/display.py | 48 ++--- app/personnalisation/ajout.py | 14 +- app/personnalisation/import_export.py | 23 ++- app/personnalisation/interface.py | 15 +- app/personnalisation/modification.py | 22 +- app/visualisations/graphes.py | 36 ++-- app/visualisations/interface.py | 33 +-- assets/locales/en copy.json | 276 ++++++++++++++++++++++++++ components/connexion.py | 22 +- components/footer.py | 6 +- components/header.py | 6 +- components/sidebar.py | 34 ++-- fabnum.py | 10 +- utils/translations.py | 6 +- utils/visualisation.py | 30 +-- 20 files changed, 512 insertions(+), 264 deletions(-) create mode 100644 assets/locales/en copy.json 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"""

{titre}

-

{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"""
-

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

+

{_("header.title")}

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

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

" + header += f"

🔧 {_("app.dev_mode")}

" else: - header += f"

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

" + header += f"

{_("header.subtitle")}

" header += """
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""" -