diff --git a/core/story-manager/src/sys_lib.h b/core/story-manager/src/sys_lib.h index 0d5936e..6a222ff 100644 --- a/core/story-manager/src/sys_lib.h +++ b/core/story-manager/src/sys_lib.h @@ -17,4 +17,7 @@ public: static std::string ToUpper(const std::string &input); static std::string ToLower(const std::string &input); static std::string ReadFile(const std::filesystem::path &filename); + static bool FileExists(const std::string& path) { + return std::filesystem::exists(path); + } }; diff --git a/story-editor/CMakeLists.txt b/story-editor/CMakeLists.txt index a733d9b..5b2c239 100644 --- a/story-editor/CMakeLists.txt +++ b/story-editor/CMakeLists.txt @@ -357,6 +357,8 @@ elseif(WIN32) ) endif() + + set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/OUTPUT" CACHE PATH "Directory for sbnw installation" FORCE) # set_target_properties(${STORY_EDITOR_PROJECT} diff --git a/story-editor/locales/de.json b/story-editor/locales/de.json new file mode 100644 index 0000000..ae3a834 --- /dev/null +++ b/story-editor/locales/de.json @@ -0,0 +1,55 @@ +{ + "language.name": "Deutsch", + "language.flag": "🇩🇪", + + "menu.language": "Sprache", + "menu.file": "Datei", + "menu.new": "Neu", + "menu.open": "Öffnen", + "menu.save": "Speichern", + "menu.quit": "Beenden", + "menu.edit": "Bearbeiten", + "menu.undo": "Rückgängig", + "menu.redo": "Wiederholen", + "menu.cut": "Ausschneiden", + "menu.copy": "Kopieren", + "menu.paste": "Einfügen", + "menu.view": "Ansicht", + "menu.help": "Hilfe", + "menu.documentation": "Dokumentation", + "menu.about": "Über", + + "window.resources": "Ressourcen", + "window.emulator": "Emulator", + "window.console": "Konsole", + "window.library": "Bibliothek", + "window.properties": "Eigenschaften", + + "resources.add_sound": "Sound hinzufügen", + "resources.add_image": "Bild hinzufügen", + "resources.choose_file": "Datei auswählen", + "resources.file": "Datei", + "resources.format": "Format", + "resources.description": "Beschreibung", + "resources.type": "Typ", + "resources.actions": "Aktionen", + + "emulator.build_nodes": "Knoten erstellen", + "emulator.build_code": "Code erstellen", + "emulator.play": "Abspielen", + "emulator.stop": "Stoppen", + "emulator.vm_state": "VM-Status: {0}", + "emulator.load_binary": "Binäre Geschichte laden (.c32)", + "emulator.set_source": "Quelldatei festlegen (.chip32)", + "emulator.choose_binary": "Binäre Geschichte auswählen", + + "dialog.ok": "OK", + "dialog.cancel": "Abbrechen", + "dialog.save": "Speichern", + "dialog.open": "Öffnen", + + "message.file_saved": "Datei erfolgreich gespeichert", + "message.file_loaded": "Datei erfolgreich geladen", + "message.error": "Fehler: {0}", + "message.build_success": "Erfolgreich erstellt" +} \ No newline at end of file diff --git a/story-editor/locales/en.json b/story-editor/locales/en.json new file mode 100644 index 0000000..6717022 --- /dev/null +++ b/story-editor/locales/en.json @@ -0,0 +1,55 @@ +{ + "language.name": "English", + "language.flag": "🇬🇧", + + "menu.language": "Language", + "menu.file": "File", + "menu.new": "New", + "menu.open": "Open", + "menu.save": "Save", + "menu.quit": "Quit", + "menu.edit": "Edit", + "menu.undo": "Undo", + "menu.redo": "Redo", + "menu.cut": "Cut", + "menu.copy": "Copy", + "menu.paste": "Paste", + "menu.view": "View", + "menu.help": "Help", + "menu.documentation": "Documentation", + "menu.about": "About", + + "window.resources": "Resources", + "window.emulator": "Emulator", + "window.console": "Console", + "window.library": "Library", + "window.properties": "Properties", + + "resources.add_sound": "Add sound", + "resources.add_image": "Add image", + "resources.choose_file": "Choose File", + "resources.file": "File", + "resources.format": "Format", + "resources.description": "Description", + "resources.type": "Type", + "resources.actions": "Actions", + + "emulator.build_nodes": "Build nodes", + "emulator.build_code": "Build code", + "emulator.play": "Play", + "emulator.stop": "Stop", + "emulator.vm_state": "VM state: {0}", + "emulator.load_binary": "Load binary story (.c32)", + "emulator.set_source": "Set script source file (.chip32)", + "emulator.choose_binary": "Choose a binary story", + + "dialog.ok": "OK", + "dialog.cancel": "Cancel", + "dialog.save": "Save", + "dialog.open": "Open", + + "message.file_saved": "File saved successfully", + "message.file_loaded": "File loaded successfully", + "message.error": "Error: {0}", + "message.build_success": "Build successful" +} \ No newline at end of file diff --git a/story-editor/locales/es.json b/story-editor/locales/es.json new file mode 100644 index 0000000..edd007b --- /dev/null +++ b/story-editor/locales/es.json @@ -0,0 +1,55 @@ +{ + "language.name": "Español", + "language.flag": "🇪🇸", + + "menu.language": "Idioma", + "menu.file": "Archivo", + "menu.new": "Nuevo", + "menu.open": "Abrir", + "menu.save": "Guardar", + "menu.quit": "Salir", + "menu.edit": "Editar", + "menu.undo": "Deshacer", + "menu.redo": "Rehacer", + "menu.cut": "Cortar", + "menu.copy": "Copiar", + "menu.paste": "Pegar", + "menu.view": "Ver", + "menu.help": "Ayuda", + "menu.documentation": "Documentación", + "menu.about": "Acerca de", + + "window.resources": "Recursos", + "window.emulator": "Emulador", + "window.console": "Consola", + "window.library": "Biblioteca", + "window.properties": "Propiedades", + + "resources.add_sound": "Añadir sonido", + "resources.add_image": "Añadir imagen", + "resources.choose_file": "Elegir archivo", + "resources.file": "Archivo", + "resources.format": "Formato", + "resources.description": "Descripción", + "resources.type": "Tipo", + "resources.actions": "Acciones", + + "emulator.build_nodes": "Compilar nodos", + "emulator.build_code": "Compilar código", + "emulator.play": "Reproducir", + "emulator.stop": "Detener", + "emulator.vm_state": "Estado VM: {0}", + "emulator.load_binary": "Cargar historia binaria (.c32)", + "emulator.set_source": "Establecer archivo fuente (.chip32)", + "emulator.choose_binary": "Elegir una historia binaria", + + "dialog.ok": "OK", + "dialog.cancel": "Cancelar", + "dialog.save": "Guardar", + "dialog.open": "Abrir", + + "message.file_saved": "Archivo guardado correctamente", + "message.file_loaded": "Archivo cargado correctamente", + "message.error": "Error: {0}", + "message.build_success": "Compilación exitosa" +} \ No newline at end of file diff --git a/story-editor/locales/fr.json b/story-editor/locales/fr.json new file mode 100644 index 0000000..52c9ba0 --- /dev/null +++ b/story-editor/locales/fr.json @@ -0,0 +1,55 @@ +{ + "language.name": "Français", + "language.flag": "🇫🇷", + + "menu.language": "Langue", + "menu.file": "Fichier", + "menu.new": "Nouveau", + "menu.open": "Ouvrir", + "menu.save": "Enregistrer", + "menu.quit": "Quitter", + "menu.edit": "Édition", + "menu.undo": "Annuler", + "menu.redo": "Refaire", + "menu.cut": "Couper", + "menu.copy": "Copier", + "menu.paste": "Coller", + "menu.view": "Affichage", + "menu.help": "Aide", + "menu.documentation": "Documentation", + "menu.about": "À propos", + + "window.resources": "Ressources", + "window.emulator": "Émulateur", + "window.console": "Console", + "window.library": "Bibliothèque", + "window.properties": "Propriétés", + + "resources.add_sound": "Ajouter un son", + "resources.add_image": "Ajouter une image", + "resources.choose_file": "Choisir un fichier", + "resources.file": "Fichier", + "resources.format": "Format", + "resources.description": "Description", + "resources.type": "Type", + "resources.actions": "Actions", + + "emulator.build_nodes": "Compiler les nœuds", + "emulator.build_code": "Compiler le code", + "emulator.play": "Lire", + "emulator.stop": "Arrêter", + "emulator.vm_state": "État VM : {0}", + "emulator.load_binary": "Charger une histoire binaire (.c32)", + "emulator.set_source": "Définir le fichier source (.chip32)", + "emulator.choose_binary": "Choisir une histoire binaire", + + "dialog.ok": "OK", + "dialog.cancel": "Annuler", + "dialog.save": "Enregistrer", + "dialog.open": "Ouvrir", + + "message.file_saved": "Fichier enregistré avec succès", + "message.file_loaded": "Fichier chargé avec succès", + "message.error": "Erreur : {0}", + "message.build_success": "Compilation réussie" +} \ No newline at end of file diff --git a/story-editor/src/LanguageSelector.h b/story-editor/src/LanguageSelector.h new file mode 100644 index 0000000..78f7b89 --- /dev/null +++ b/story-editor/src/LanguageSelector.h @@ -0,0 +1,74 @@ +#ifndef LANGUAGE_SELECTOR_H +#define LANGUAGE_SELECTOR_H + +#include +#include +#include "imgui.h" +#include "Localization.h" + +class LanguageSelector { +public: + LanguageSelector() = default; + ~LanguageSelector() = default; + + // Callback appelé quand la langue change + using OnLanguageChangedCallback = std::function; + + void SetOnLanguageChanged(OnLanguageChangedCallback callback) { + m_onLanguageChanged = callback; + } + + // Dessiner le menu de sélection de langue + void DrawMenu() { + // Nom du menu qui se met à jour avec la langue + const char* menuLabel = TR("menu.language"); + + if (ImGui::BeginMenu(menuLabel)) { + DrawLanguageOptions(); + ImGui::EndMenu(); + } + } + +private: + void DrawLanguageOptions() { + auto& localization = Localization::Instance(); + const auto& languages = localization.GetAvailableLanguages(); + const std::string& currentLang = localization.GetCurrentLang(); + + if (languages.empty()) { + ImGui::TextDisabled("No languages available"); + return; + } + + for (const auto& lang : languages) { + // Vérifier si c'est la langue actuelle + bool isSelected = (lang.code == currentLang); + + // Construire le label avec emoji si disponible + std::string label; + if (!lang.flagEmoji.empty()) { + // FIXME: ImGui ne supporte pas en natif les emojis ; chercher sur le net + // label = lang.flagEmoji + " " + lang.displayName; + label = lang.displayName; + } else { + label = lang.displayName; + } + + // MenuItem avec état de sélection (affiche une coche) + if (ImGui::MenuItem(label.c_str(), nullptr, isSelected)) { + if (!isSelected) { // Seulement si ce n'est pas déjà la langue active + if (localization.LoadLanguage(lang.code)) { + // Notifier le changement + if (m_onLanguageChanged) { + m_onLanguageChanged(lang.code); + } + } + } + } + } + } + + OnLanguageChangedCallback m_onLanguageChanged; +}; + +#endif // LANGUAGE_SELECTOR_H \ No newline at end of file diff --git a/story-editor/src/Localization.h b/story-editor/src/Localization.h new file mode 100644 index 0000000..5f3be0f --- /dev/null +++ b/story-editor/src/Localization.h @@ -0,0 +1,156 @@ +#ifndef LOCALIZATION_H +#define LOCALIZATION_H + +#include +#include +#include +#include +#include +#include +#include "json.hpp" +#include "sys_lib.h" + +using json = nlohmann::json; + +struct LanguageInfo { + std::string code; // "en", "fr", etc. + std::string displayName; // "English", "Français", etc. + std::string flagEmoji; // "🇬🇧", "🇫🇷", etc. (optionnel) +}; + +class Localization { +public: + static Localization& Instance() { + static Localization instance; + return instance; + } + + // Scanner le dossier locales/ pour détecter les langues disponibles + void ScanAvailableLanguages() { + m_availableLanguages.clear(); + + std::string localesPath = "locales"; + + if (!std::filesystem::exists(localesPath)) { + return; + } + + for (const auto& entry : std::filesystem::directory_iterator(localesPath)) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + std::string langCode = entry.path().stem().string(); + + try { + std::string content = SysLib::ReadFile(entry.path().string()); + json j = json::parse(content); + + LanguageInfo info; + info.code = langCode; + + // Lire le nom d'affichage depuis le fichier de langue + if (j.contains("language.name")) { + info.displayName = j["language.name"].get(); + } else { + info.displayName = langCode; // Fallback sur le code + } + + // Lire l'emoji de drapeau (optionnel) + if (j.contains("language.flag")) { + info.flagEmoji = j["language.flag"].get(); + } + + m_availableLanguages.push_back(info); + } + catch (const std::exception& e) { + // Ignorer les fichiers JSON invalides + continue; + } + } + } + + // Trier par code de langue + std::sort(m_availableLanguages.begin(), m_availableLanguages.end(), + [](const LanguageInfo& a, const LanguageInfo& b) { + return a.code < b.code; + }); + } + + const std::vector& GetAvailableLanguages() const { + return m_availableLanguages; + } + + // Charger un fichier de langue + bool LoadLanguage(const std::string& langCode) { + std::string filePath = "locales/" + langCode + ".json"; + + if (!SysLib::FileExists(filePath)) { + return false; + } + + try { + std::string content = SysLib::ReadFile(filePath); + json j = json::parse(content); + + m_translations.clear(); + for (auto& [key, value] : j.items()) { + m_translations[key] = value.get(); + } + + m_currentLang = langCode; + return true; + } + catch (const std::exception& e) { + return false; + } + } + + // Obtenir une traduction + const char* Get(const std::string& key) const { + auto it = m_translations.find(key); + if (it != m_translations.end()) { + return it->second.c_str(); + } + // Retourne la clé si non trouvée (utile pour le debug) + return key.c_str(); + } + + // Obtenir avec des paramètres de format + std::string GetF(const std::string& key, const std::vector& args) const { + std::string text = Get(key); + + for (size_t i = 0; i < args.size(); ++i) { + std::string placeholder = "{" + std::to_string(i) + "}"; + size_t pos = text.find(placeholder); + if (pos != std::string::npos) { + text.replace(pos, placeholder.length(), args[i]); + } + } + + return text; + } + + const std::string& GetCurrentLang() const { + return m_currentLang; + } + +private: + Localization() : m_currentLang("en") { + // Scanner les langues disponibles au démarrage + ScanAvailableLanguages(); + // Charger la langue par défaut + LoadLanguage("en"); + } + + // Empêcher la copie + Localization(const Localization&) = delete; + Localization& operator=(const Localization&) = delete; + + std::unordered_map m_translations; + std::string m_currentLang; + std::vector m_availableLanguages; +}; + +// Macro pour simplifier l'usage +#define TR(key) Localization::Instance().Get(key) +#define TRF(key, ...) Localization::Instance().GetF(key, {__VA_ARGS__}) + +#endif // LOCALIZATION_H \ No newline at end of file diff --git a/story-editor/src/app/app_controller.cpp b/story-editor/src/app/app_controller.cpp index 1d52c25..2cfbd6f 100644 --- a/story-editor/src/app/app_controller.cpp +++ b/story-editor/src/app/app_controller.cpp @@ -15,9 +15,9 @@ #include "json.hpp" #include "variable.h" // Pour Variable #include "all_events.h" +#include "Localization.h" // Définitions des registres et événements CHIP-32 si non déjà dans chip32_vm.h -// Assurez-vous que ces définitions sont accessibles. #ifndef R0 #define R0 0 #endif @@ -519,6 +519,7 @@ void AppController::SaveParams() j["recents"] = m_recentProjects; j["library_path"] = m_libraryManager.LibraryPath(); j["store_url"] = m_libraryManager.GetStoreUrl(); + j["language"] = Localization::Instance().GetCurrentLang(); std::string loc = pf::getConfigHome() + "/ost_settings.json"; std::ofstream o(loc); @@ -563,6 +564,11 @@ void AppController::LoadParams() m_logger.Log("No 'store_url' found in settings, using default.", false); } + if (j.contains("language")) { + std::string lang = j["language"].get(); + Localization::Instance().LoadLanguage(lang); + } + } catch(const std::exception &e) { diff --git a/story-editor/src/main_window.cpp b/story-editor/src/main_window.cpp index 767bd38..6cf9cd3 100644 --- a/story-editor/src/main_window.cpp +++ b/story-editor/src/main_window.cpp @@ -106,6 +106,13 @@ MainWindow::MainWindow(ILogger& logger, EventBus& eventBus, AppController& appCo m_toastNotifier.addToast("Module", "Module build failed", ToastType::Error); } }); + + + m_languageSelector.SetOnLanguageChanged([this](const std::string& langCode) { + // Sauvegarder la préférence + m_appController.SaveParams(); + m_logger.Log("Language changed to: " + langCode); + }); } MainWindow::~MainWindow() @@ -123,7 +130,7 @@ float MainWindow::DrawMainMenuBar() if (ImGui::BeginMainMenuBar()) { - if (ImGui::BeginMenu("File")) + if (ImGui::BeginMenu(TR("menu.file"))) { if (ImGui::MenuItem("New story project")) @@ -190,7 +197,7 @@ float MainWindow::DrawMainMenuBar() NewModule(); } - if (ImGui::MenuItem("Save module")) + if (ImGui::MenuItem("Save module (ctrl+s)")) { SaveModule(); } @@ -204,6 +211,16 @@ float MainWindow::DrawMainMenuBar() ImGui::EndMenu(); } + // if (ImGui::BeginMenu(TR("menu.view"))) { + // ImGui::MenuItem(TR("window.console"), nullptr, &m_showConsole); + // ImGui::MenuItem(TR("window.resources"), nullptr, &m_showResources); + // ImGui::MenuItem(TR("window.properties"), nullptr, &m_showProperties); + // ImGui::EndMenu(); + // } + + m_languageSelector.DrawMenu(); + + if (ImGui::BeginMenu("Help")) { if (ImGui::MenuItem("About")) @@ -261,7 +278,7 @@ bool MainWindow::ShowQuitConfirm() // ImGui::SetNextWindowSize(ImVec2(200, 150)); if (ImGui::BeginPopupModal("QuitConfirm", NULL, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::Text("Really qui without saving?"); + ImGui::Text("Really quit without saving?"); ImGui::Separator(); if (ImGui::Button("OK", ImVec2(120, 0))) @@ -520,7 +537,11 @@ bool MainWindow::Loop() if (m_appController.IsLibraryManagerInitialized()) { + + bool nodeEditorFocused = m_nodeEditorWindow.IsFocused(); + bool moduleEditorFocused = m_moduleEditorWindow.IsFocused(); + m_consoleWindow.Draw(); m_emulatorDock.Draw(); m_debuggerWindow.Draw(); @@ -528,7 +549,6 @@ bool MainWindow::Loop() m_nodeEditorWindow.Draw(); m_moduleEditorWindow.Draw(); - auto currentStory = nodeEditorFocused ? m_nodeEditorWindow.GetCurrentStory() : m_moduleEditorWindow.GetCurrentStory(); m_variablesWindow.Draw(currentStory); m_cpuWindow.Draw(); @@ -546,6 +566,31 @@ bool MainWindow::Loop() // DockingToolbar("Toolbar2", &toolbar2_axis); DrawToolBar(height); + + ImGuiIO& io = ImGui::GetIO(); + if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S, false)) + { + if (moduleEditorFocused && m_module) + { + // Si l'éditeur de module a le focus, sauvegarder le module + m_appController.SaveModule(); + m_toastNotifier.success("Module sauvegardé"); + m_logger.Log("Module sauvegardé via Ctrl+S"); + } + else if (m_story) + { + // Sinon, sauvegarder l'histoire principale + m_appController.SaveProject(); + m_toastNotifier.success("Projet sauvegardé"); + m_logger.Log("Projet sauvegardé via Ctrl+S"); + } + else + { + // Aucun projet ouvert + m_toastNotifier.warning("Aucun projet ou module à sauvegarder"); + m_logger.Log("Tentative de sauvegarde sans projet ouvert", true); + } + } } m_aboutDialog.Draw(); diff --git a/story-editor/src/main_window.h b/story-editor/src/main_window.h index 2f5b600..8e06be0 100644 --- a/story-editor/src/main_window.h +++ b/story-editor/src/main_window.h @@ -17,6 +17,7 @@ #include "variables_window.h" #include "library_window.h" #include "cpu_window.h" + // Dialogs #include "about_dialog.h" #include "project_properties_dialog.h" @@ -26,6 +27,8 @@ #include "i_logger.h" #include "imgui_toast_notifier.h" #include "node_widget_factory.h" +#include "Localization.h" +#include "LanguageSelector.h" class MainWindow : public std::enable_shared_from_this, public ILogSubject { @@ -67,6 +70,7 @@ private: ProjectPropertiesDialog m_projectPropertiesDialog; ImGuiToastNotifier m_toastNotifier; + LanguageSelector m_languageSelector; // From IStoryManager (proxy to StoryProject class) void OpenProject(const std::string &uuid);