feat(locale): sistema i18n YAML amb català i anglès
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user