From 837eb733989a8b5f7e149e2515348ce9aaca310c Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Sun, 19 Apr 2026 12:11:42 +0200 Subject: [PATCH] =?UTF-8?q?Locale=20amb=20catal=C3=A0=20(defecte)=20i=20an?= =?UTF-8?q?gl=C3=A8s;=20Options.locale=20persistit=20a=20config.yaml=20(v0?= =?UTF-8?q?.10.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 1 + data/locale/ca.yaml | 5 +++ data/locale/en.yaml | 5 +++ source/core/locale/locale.cpp | 57 +++++++++++++++++++++++++++++++ source/core/locale/locale.hpp | 34 ++++++++++++++++++ source/core/system/director.cpp | 5 +++ source/game/options.cpp | 4 ++- source/game/options.hpp | 1 + source/game/scenes/game_scene.cpp | 3 +- source/utils/defaults.hpp | 5 +++ source/utils/defines.hpp | 2 +- 11 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 data/locale/ca.yaml create mode 100644 data/locale/en.yaml create mode 100644 source/core/locale/locale.cpp create mode 100644 source/core/locale/locale.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9396f12..3f3e850 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ configure_file(${CMAKE_SOURCE_DIR}/source/version.h.in ${CMAKE_BINARY_DIR}/versi set(APP_SOURCES source/main.cpp source/core/system/director.cpp + source/core/locale/locale.cpp source/core/rendering/screen.cpp source/core/rendering/notifier.cpp source/core/rendering/render_info.cpp diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml new file mode 100644 index 0000000..58e52d3 --- /dev/null +++ b/data/locale/ca.yaml @@ -0,0 +1,5 @@ +# AEE 2026 — Català +# lang: ca + +notification: + test: "notificacio de prova" diff --git a/data/locale/en.yaml b/data/locale/en.yaml new file mode 100644 index 0000000..86ee3d9 --- /dev/null +++ b/data/locale/en.yaml @@ -0,0 +1,5 @@ +# AEE 2026 — English +# lang: en + +notification: + test: "test notification" diff --git a/source/core/locale/locale.cpp b/source/core/locale/locale.cpp new file mode 100644 index 0000000..4a208c1 --- /dev/null +++ b/source/core/locale/locale.cpp @@ -0,0 +1,57 @@ +#include "core/locale/locale.hpp" + +#include +#include +#include + +#include "external/fkyaml_node.hpp" + +namespace { + + // Recorre el node YAML i aplana jerarquies en claus "a.b.c". Claus no + // cadena (p.ex. llistes o nombres solts) es ignoren silenciosament — + // només textos útils per traduir acaben al mapping. + void flatten(const fkyaml::node& node, const std::string& prefix, std::unordered_map& out) { + for (auto it = node.begin(); it != node.end(); ++it) { + const std::string KEY = prefix.empty() + ? it.key().get_value() + : prefix + "." + it.key().get_value(); + const auto& value = it.value(); + if (value.is_mapping()) { + flatten(value, KEY, out); + } else if (value.is_string()) { + out[KEY] = value.get_value(); + } + } + } + +} // namespace + +auto Locale::get() -> Locale& { + static Locale instance_; + return instance_; +} + +void Locale::load(const std::string& file_path) { + std::ifstream file(file_path); + if (!file.is_open()) { + std::cerr << "Locale: no s'ha pogut obrir " << file_path << '\n'; + return; + } + + try { + auto yaml = fkyaml::node::deserialize(file); + strings_.clear(); + flatten(yaml, "", strings_); + std::cout << "Locale: " << strings_.size() << " traduccions des de " << file_path << '\n'; + } catch (const std::exception& e) { + std::cerr << "Locale: error parsejant " << file_path << ": " << e.what() << '\n'; + } +} + +auto Locale::text(const std::string& key) const -> std::string { + auto it = strings_.find(key); + if (it != strings_.end()) { return it->second; } + std::cerr << "Locale: clau no trobada: " << key << '\n'; + return key; +} diff --git a/source/core/locale/locale.hpp b/source/core/locale/locale.hpp new file mode 100644 index 0000000..2eb4c3b --- /dev/null +++ b/source/core/locale/locale.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +// Locale amb claus en notació de punts ("notification.test"). El YAML pot ser +// jerarquitzat però s'aplana en càrrega, així el consumidor només veu claus +// planes. No hi ha hot-swap d'idioma — l'idioma es fixa a l'arrencada via +// Options i es recàrrega només amb reinici del joc. +class Locale { + public: + static auto get() -> Locale&; + + Locale(const Locale&) = delete; + Locale(Locale&&) = delete; + auto operator=(const Locale&) -> Locale& = delete; + auto operator=(Locale&&) -> Locale& = delete; + + // Llig el fitxer YAML i emplena el mapping intern. Si hi ha un error de + // parse o el fitxer no existeix, deixa el mapping com estava i ho + // notifica per stderr. + void load(const std::string& file_path); + + // Retorna la traducció; si la clau no existeix, retorna la pròpia clau + // com a fallback visible (així una clau mal escrita és detectable sense + // trencar el render). + [[nodiscard]] auto text(const std::string& key) const -> std::string; + + private: + Locale() = default; + ~Locale() = default; + + std::unordered_map strings_; +}; diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index 7205a0a..f13c8b5 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -10,6 +10,7 @@ #include #include +#include "core/locale/locale.hpp" #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" @@ -54,6 +55,10 @@ Director::Director() { Screen::get().applyCrtPiParams(Options::buildCrtPiParams()); (void)Resource::Cache::get(); + // Traduccions per a tots els textos visibles. Si el fitxer no existeix + // o hi ha un error de parse, Locale::text() caurà a retornar la clau. + Locale::get().load(std::string(Defaults::I18n::LOCALE_DIR) + Options::locale + ".yaml"); + // Recursos compartits d'UI (paleta + font). Es carreguen abans que els // overlays perquè tots els reaprofiten sense duplicar la lògica. Resource::Cache::get().loadPalette("ui", "data/palettes/sega-cube.pal"); diff --git a/source/game/options.cpp b/source/game/options.cpp index 7ec04d7..22034b8 100644 --- a/source/game/options.cpp +++ b/source/game/options.cpp @@ -258,6 +258,7 @@ namespace Options { if (file_version != Texts::VERSION) { return saveToFile(); } config_version = file_version; + if (yaml.contains("locale")) { Options::locale = yaml["locale"].get_value(); } parseWindow(yaml); parseVideo(yaml); parseAudio(yaml); @@ -276,7 +277,8 @@ namespace Options { std::ostringstream out; out << "# Configuració d'AEE 2026. Generat automàticament; editable a mà.\n"; - out << "version: \"" << config_version << "\"\n\n"; + out << "version: \"" << config_version << "\"\n"; + out << "locale: \"" << locale << "\"\n\n"; out << "window:\n"; out << " zoom: " << window.zoom << "\n"; diff --git a/source/game/options.hpp b/source/game/options.hpp index 06cb32b..3aa9cfa 100644 --- a/source/game/options.hpp +++ b/source/game/options.hpp @@ -94,6 +94,7 @@ namespace Options { inline std::string config_file_path{}; inline std::string postfx_file_path{}; inline std::string crtpi_file_path{}; + inline std::string locale{Defaults::I18n::LOCALE}; inline Window window{}; inline Video video{}; inline Audio audio{}; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index f98fe9b..9acfa00 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -2,6 +2,7 @@ #include +#include "core/locale/locale.hpp" #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" @@ -36,7 +37,7 @@ void GameScene::update(int delta_ms) { if (event.type == SDL_EVENT_KEY_DOWN) { if (event.key.scancode == SDL_SCANCODE_ESCAPE) { wants_quit_ = true; } if (event.key.scancode == SDL_SCANCODE_F3) { RenderInfo::get().toggle(); } - if (event.key.scancode == SDL_SCANCODE_F4) { Notifier::get().notify("notificacio de prova"); } + if (event.key.scancode == SDL_SCANCODE_F4) { Notifier::get().notify(Locale::get().text("notification.test")); } } } diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 4934699..ad415e2 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -28,6 +28,11 @@ namespace Defaults { constexpr bool FULLSCREEN = false; } // namespace Window + namespace I18n { + constexpr const char* LOCALE = "ca"; // Idioma per defecte (codi ISO 639-1). + constexpr const char* LOCALE_DIR = "data/locale/"; // Carpeta amb els YAML per idioma. + } // namespace I18n + namespace Video { constexpr bool VSYNC = true; constexpr ScalingMode SCALING_MODE = ScalingMode::LETTERBOX; diff --git a/source/utils/defines.hpp b/source/utils/defines.hpp index 4897c9e..6ef9cd3 100644 --- a/source/utils/defines.hpp +++ b/source/utils/defines.hpp @@ -3,7 +3,7 @@ namespace Texts { constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte — JailDesigner"; constexpr const char* COPYRIGHT = "© 2026 JailDesigner"; - constexpr const char* VERSION = "0.9.0"; // Single source of truth — Makefile el grep + constexpr const char* VERSION = "0.10.0"; // Single source of truth — Makefile el grep } // namespace Texts namespace GameCanvas {