feat(locale): sistema i18n YAML amb català i anglès

This commit is contained in:
2026-05-24 10:28:56 +02:00
parent 274ce1ca63
commit 35d720bb77
13 changed files with 446 additions and 180 deletions
+1
View File
@@ -64,6 +64,7 @@ namespace Config {
KeyboardBindings keyboard_controls{}; // Defaults globals per Input
GamepadBindings gamepad_controls{};
bool console{false};
std::string locale{"ca"}; // "ca" | "en" — fixat a l'arrencada, sense hot-swap
};
} // namespace Config
+103
View File
@@ -0,0 +1,103 @@
// locale.cpp - Implementació del sistema de locale
// © 2026 JailDesigner
#include "core/locale/locale.hpp"
#include <cstddef>
#include <cstdint>
#include <exception>
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
namespace {
// Recorre el node YAML i aplana jerarquies en claus "a.b.c". Suporta
// mappings (recursió) i seqüències de strings (desa "a.b.0", "a.b.1"...).
// Altres tipus (nombres, booleans solts) s'ignoren silenciosament.
void flatten(const fkyaml::node& node, const std::string& prefix, std::unordered_map<std::string, std::string>& out) {
if (node.is_mapping()) {
for (auto it = node.begin(); it != node.end(); ++it) {
const std::string KEY = prefix.empty()
? it.key().get_value<std::string>()
: prefix + "." + it.key().get_value<std::string>();
flatten(it.value(), KEY, out);
}
return;
}
if (node.is_sequence()) {
std::size_t index = 0;
for (const auto& item : node) {
const std::string KEY = prefix + "." + std::to_string(index);
flatten(item, KEY, out);
index++;
}
return;
}
if (node.is_string()) {
out[prefix] = node.get_value<std::string>();
}
}
} // namespace
auto Locale::get() -> Locale& {
static Locale instance_;
return instance_;
}
void Locale::load(const std::string& file_path) {
// Normalitza traient prefix "data/" com fa StageLoader: el pack de
// recursos indexa rutes relatives a `data/`.
std::string normalized = file_path;
if (normalized.starts_with("data/")) {
normalized = normalized.substr(5);
}
std::vector<uint8_t> bytes = Resource::Helper::loadFile(normalized);
if (bytes.empty()) {
std::cerr << "[Locale] no s'ha pogut load " << normalized << '\n';
return;
}
try {
std::string yaml_content(bytes.begin(), bytes.end());
std::stringstream stream(yaml_content);
fkyaml::node yaml = fkyaml::node::deserialize(stream);
strings_.clear();
flatten(yaml, "", strings_);
std::cout << "[Locale] " << strings_.size() << " traduccions des de " << normalized << '\n';
} catch (const std::exception& e) {
std::cerr << "[Locale] error parsejant " << normalized << ": " << 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;
}
auto Locale::count(const std::string& prefix) const -> std::size_t {
std::size_t n = 0;
while (strings_.contains(prefix + "." + std::to_string(n))) {
n++;
}
return n;
}
auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string {
auto pos = tpl.find(placeholder);
if (pos != std::string::npos) {
tpl.replace(pos, placeholder.size(), value);
}
return tpl;
}
+49
View File
@@ -0,0 +1,49 @@
// locale.hpp - Sistema d'internacionalització (i18n) basat en YAML
// © 2026 JailDesigner
//
// Locale amb claus en notació de punts ("notification.fullscreen_on"). El YAML
// pot ser jerarquitzat i s'aplana en càrrega, així el consumidor només veu
// claus planes. Suporta seqüències de strings (es desen com prefix.0,
// prefix.1, ...). No hi ha hot-swap d'idioma: es fixa a l'arrencada des de
// `config.yaml` (camp `locale`) i només es recarrega reiniciant el joc.
#pragma once
#include <cstddef>
#include <string>
#include <string_view>
#include <unordered_map>
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 es detecta sense
// trencar el render).
[[nodiscard]] auto text(const std::string& key) const -> std::string;
// Compta quantes claus consecutives existeixen amb el prefix donat
// (prefix.0, prefix.1, ...). Útil per pools indexats com stage.start.N.
[[nodiscard]] auto count(const std::string& prefix) const -> std::size_t;
private:
Locale() = default;
~Locale() = default;
std::unordered_map<std::string, std::string> strings_;
};
// Substitució simple d'un placeholder dins una plantilla (p.ex. "{n}" → "3").
// S'usa per interpolar valors runtime en strings traduïdes.
[[nodiscard]] auto localeSubstitute(std::string tpl, std::string_view placeholder, std::string_view value) -> std::string;
+13 -6
View File
@@ -14,6 +14,7 @@
#include "core/defaults/rendering.hpp"
#include "core/defaults/window.hpp"
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/coordinate_transform.hpp"
#include "core/system/notifier.hpp"
#include "project.h"
@@ -250,7 +251,10 @@ void SDLManager::increaseWindowSize() {
float new_zoom = zoom_factor_ + Defaults::Window::ZOOM_INCREMENT;
applyZoom(new_zoom);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_));
notifier->notifyInfo(localeSubstitute(
Locale::get().text("notification.zoom"),
"{z}",
std::format("{:.1f}", zoom_factor_)));
}
}
@@ -261,7 +265,10 @@ void SDLManager::decreaseWindowSize() {
float new_zoom = zoom_factor_ - Defaults::Window::ZOOM_INCREMENT;
applyZoom(new_zoom);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(std::format("ZOOM: {:.1f}X", zoom_factor_));
notifier->notifyInfo(localeSubstitute(
Locale::get().text("notification.zoom"),
"{z}",
std::format("{:.1f}", zoom_factor_)));
}
}
@@ -310,7 +317,7 @@ void SDLManager::toggleFullscreen() {
Mouse::setForceHidden(is_fullscreen_);
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(is_fullscreen_ ? "PANTALLA COMPLETA" : "MODE FINESTRA");
notifier->notifyInfo(Locale::get().text(is_fullscreen_ ? "notification.fullscreen_on" : "notification.fullscreen_off"));
}
}
@@ -364,7 +371,7 @@ void SDLManager::toggleVSync() {
on_persist_();
}
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(cfg_->rendering.vsync != 0 ? "VSYNC ACTIU" : "VSYNC INACTIU");
notifier->notifyInfo(Locale::get().text(cfg_->rendering.vsync != 0 ? "notification.vsync_on" : "notification.vsync_off"));
}
}
@@ -374,7 +381,7 @@ void SDLManager::toggleAntialias() {
// No persistim: l'AA és toggleable runtime però el seu estat no es
// guarda al YAML de moment (decisió volgudament conservadora).
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(cfg_->rendering.antialias != 0 ? "AA ACTIU" : "AA INACTIU");
notifier->notifyInfo(Locale::get().text(cfg_->rendering.antialias != 0 ? "notification.antialias_on" : "notification.antialias_off"));
}
}
@@ -384,6 +391,6 @@ void SDLManager::togglePostFx() {
// No persistim: el toggle és per A/B testing visual, l'estat per defecte
// del joc continua sent "postfx ON" segons defaults/YAML.
if (auto* notifier = System::Notifier::get(); notifier != nullptr) {
notifier->notifyInfo(NEW_STATE ? "POSTPROCESSAT ACTIU" : "POSTPROCESSAT INACTIU");
notifier->notifyInfo(Locale::get().text(NEW_STATE ? "notification.postfx_on" : "notification.postfx_off"));
}
}
+5
View File
@@ -15,6 +15,7 @@
#include "core/defaults/window.hpp"
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_loader.hpp"
@@ -99,6 +100,10 @@ Director::Director(int argc, char* argv[])
// Carregar o crear configuración
ConfigYaml::loadFromFile();
// Carregar locale segons la config (per defecte "ca"). Si la càrrega
// falla, Locale::text() retorna la clau crua i el joc segueix funcionant.
Locale::get().load(std::string("locale/") + cfg_->locale + ".yaml");
// Inicialitzar sistema de input
Input::init("data/gamecontrollerdb.txt");
+2 -1
View File
@@ -7,6 +7,7 @@
#include "core/input/input.hpp"
#include "core/input/mouse.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp"
#include "core/system/notifier.hpp"
#include "scene_context.hpp"
@@ -71,7 +72,7 @@ namespace GlobalEvents {
// sortida en lloc de tancar.
auto* notifier = System::Notifier::get();
if (notifier != nullptr && !notifier->isExitPromptActive()) {
notifier->notifyExit("PREMEU ESC UN ALTRE COP PER EIXIR");
notifier->notifyExit(Locale::get().text("notification.press_again_exit"));
return true;
}
// Notifier inexistent (degradació elegant) o segona ESC