Added localization API and helpers
Some checks are pending
Build-StoryEditor / build_linux (push) Waiting to run
Build-StoryEditor / build_win32 (push) Waiting to run
Deploy-Documentation / deploy (push) Waiting to run

This commit is contained in:
anthony@rabine.fr 2025-10-02 21:52:28 +02:00
parent 663fa47004
commit d58967710a
11 changed files with 515 additions and 5 deletions

View file

@ -17,4 +17,7 @@ public:
static std::string ToUpper(const std::string &input); static std::string ToUpper(const std::string &input);
static std::string ToLower(const std::string &input); static std::string ToLower(const std::string &input);
static std::string ReadFile(const std::filesystem::path &filename); static std::string ReadFile(const std::filesystem::path &filename);
static bool FileExists(const std::string& path) {
return std::filesystem::exists(path);
}
}; };

View file

@ -357,6 +357,8 @@ elseif(WIN32)
) )
endif() endif()
set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/OUTPUT" CACHE PATH "Directory for sbnw installation" FORCE) set(CMAKE_INSTALL_PREFIX "${CMAKE_SOURCE_DIR}/OUTPUT" CACHE PATH "Directory for sbnw installation" FORCE)
# set_target_properties(${STORY_EDITOR_PROJECT} # set_target_properties(${STORY_EDITOR_PROJECT}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -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"
}

View file

@ -0,0 +1,74 @@
#ifndef LANGUAGE_SELECTOR_H
#define LANGUAGE_SELECTOR_H
#include <functional>
#include <string>
#include "imgui.h"
#include "Localization.h"
class LanguageSelector {
public:
LanguageSelector() = default;
~LanguageSelector() = default;
// Callback appelé quand la langue change
using OnLanguageChangedCallback = std::function<void(const std::string& langCode)>;
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

View file

@ -0,0 +1,156 @@
#ifndef LOCALIZATION_H
#define LOCALIZATION_H
#include <string>
#include <unordered_map>
#include <vector>
#include <memory>
#include <filesystem>
#include <algorithm>
#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<std::string>();
} 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<std::string>();
}
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<LanguageInfo>& 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<std::string>();
}
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<std::string>& 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<std::string, std::string> m_translations;
std::string m_currentLang;
std::vector<LanguageInfo> 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

View file

@ -15,9 +15,9 @@
#include "json.hpp" #include "json.hpp"
#include "variable.h" // Pour Variable #include "variable.h" // Pour Variable
#include "all_events.h" #include "all_events.h"
#include "Localization.h"
// Définitions des registres et événements CHIP-32 si non déjà dans chip32_vm.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 #ifndef R0
#define R0 0 #define R0 0
#endif #endif
@ -519,6 +519,7 @@ void AppController::SaveParams()
j["recents"] = m_recentProjects; j["recents"] = m_recentProjects;
j["library_path"] = m_libraryManager.LibraryPath(); j["library_path"] = m_libraryManager.LibraryPath();
j["store_url"] = m_libraryManager.GetStoreUrl(); j["store_url"] = m_libraryManager.GetStoreUrl();
j["language"] = Localization::Instance().GetCurrentLang();
std::string loc = pf::getConfigHome() + "/ost_settings.json"; std::string loc = pf::getConfigHome() + "/ost_settings.json";
std::ofstream o(loc); std::ofstream o(loc);
@ -563,6 +564,11 @@ void AppController::LoadParams()
m_logger.Log("No 'store_url' found in settings, using default.", false); m_logger.Log("No 'store_url' found in settings, using default.", false);
} }
if (j.contains("language")) {
std::string lang = j["language"].get<std::string>();
Localization::Instance().LoadLanguage(lang);
}
} }
catch(const std::exception &e) catch(const std::exception &e)
{ {

View file

@ -106,6 +106,13 @@ MainWindow::MainWindow(ILogger& logger, EventBus& eventBus, AppController& appCo
m_toastNotifier.addToast("Module", "Module build failed", ToastType::Error); 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() MainWindow::~MainWindow()
@ -123,7 +130,7 @@ float MainWindow::DrawMainMenuBar()
if (ImGui::BeginMainMenuBar()) if (ImGui::BeginMainMenuBar())
{ {
if (ImGui::BeginMenu("File")) if (ImGui::BeginMenu(TR("menu.file")))
{ {
if (ImGui::MenuItem("New story project")) if (ImGui::MenuItem("New story project"))
@ -190,7 +197,7 @@ float MainWindow::DrawMainMenuBar()
NewModule(); NewModule();
} }
if (ImGui::MenuItem("Save module")) if (ImGui::MenuItem("Save module (ctrl+s)"))
{ {
SaveModule(); SaveModule();
} }
@ -204,6 +211,16 @@ float MainWindow::DrawMainMenuBar()
ImGui::EndMenu(); 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::BeginMenu("Help"))
{ {
if (ImGui::MenuItem("About")) if (ImGui::MenuItem("About"))
@ -261,7 +278,7 @@ bool MainWindow::ShowQuitConfirm()
// ImGui::SetNextWindowSize(ImVec2(200, 150)); // ImGui::SetNextWindowSize(ImVec2(200, 150));
if (ImGui::BeginPopupModal("QuitConfirm", NULL, ImGuiWindowFlags_AlwaysAutoResize)) if (ImGui::BeginPopupModal("QuitConfirm", NULL, ImGuiWindowFlags_AlwaysAutoResize))
{ {
ImGui::Text("Really qui without saving?"); ImGui::Text("Really quit without saving?");
ImGui::Separator(); ImGui::Separator();
if (ImGui::Button("OK", ImVec2(120, 0))) if (ImGui::Button("OK", ImVec2(120, 0)))
@ -520,7 +537,11 @@ bool MainWindow::Loop()
if (m_appController.IsLibraryManagerInitialized()) if (m_appController.IsLibraryManagerInitialized())
{ {
bool nodeEditorFocused = m_nodeEditorWindow.IsFocused(); bool nodeEditorFocused = m_nodeEditorWindow.IsFocused();
bool moduleEditorFocused = m_moduleEditorWindow.IsFocused();
m_consoleWindow.Draw(); m_consoleWindow.Draw();
m_emulatorDock.Draw(); m_emulatorDock.Draw();
m_debuggerWindow.Draw(); m_debuggerWindow.Draw();
@ -528,7 +549,6 @@ bool MainWindow::Loop()
m_nodeEditorWindow.Draw(); m_nodeEditorWindow.Draw();
m_moduleEditorWindow.Draw(); m_moduleEditorWindow.Draw();
auto currentStory = nodeEditorFocused ? m_nodeEditorWindow.GetCurrentStory() : m_moduleEditorWindow.GetCurrentStory(); auto currentStory = nodeEditorFocused ? m_nodeEditorWindow.GetCurrentStory() : m_moduleEditorWindow.GetCurrentStory();
m_variablesWindow.Draw(currentStory); m_variablesWindow.Draw(currentStory);
m_cpuWindow.Draw(); m_cpuWindow.Draw();
@ -546,6 +566,31 @@ bool MainWindow::Loop()
// DockingToolbar("Toolbar2", &toolbar2_axis); // DockingToolbar("Toolbar2", &toolbar2_axis);
DrawToolBar(height); 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(); m_aboutDialog.Draw();

View file

@ -17,6 +17,7 @@
#include "variables_window.h" #include "variables_window.h"
#include "library_window.h" #include "library_window.h"
#include "cpu_window.h" #include "cpu_window.h"
// Dialogs // Dialogs
#include "about_dialog.h" #include "about_dialog.h"
#include "project_properties_dialog.h" #include "project_properties_dialog.h"
@ -26,6 +27,8 @@
#include "i_logger.h" #include "i_logger.h"
#include "imgui_toast_notifier.h" #include "imgui_toast_notifier.h"
#include "node_widget_factory.h" #include "node_widget_factory.h"
#include "Localization.h"
#include "LanguageSelector.h"
class MainWindow : public std::enable_shared_from_this<MainWindow>, public ILogSubject class MainWindow : public std::enable_shared_from_this<MainWindow>, public ILogSubject
{ {
@ -67,6 +70,7 @@ private:
ProjectPropertiesDialog m_projectPropertiesDialog; ProjectPropertiesDialog m_projectPropertiesDialog;
ImGuiToastNotifier m_toastNotifier; ImGuiToastNotifier m_toastNotifier;
LanguageSelector m_languageSelector;
// From IStoryManager (proxy to StoryProject class) // From IStoryManager (proxy to StoryProject class)
void OpenProject(const std::string &uuid); void OpenProject(const std::string &uuid);