feature: localization (#32)

* build: add nlohmann/json library

* build(deps): add harfbuzz dependency

* feat(cmake): add guard to reject macOS builds

* feat(font): add HarfBuzz shaping and replace font with unifont

* feat(localization): add i18n support with en_US and zh_CN translations

Implement Localization class to load JSON translation files. Replace hardcoded strings in UI with translation keys and update sliders/combo buttons to use dynamic text.

* feat(localization): detect system locale for language default

* feat(settings): add language selection option with restart notification

* fix: use vckpg to add freetype on windows
This commit is contained in:
zhenyan121
2026-07-16 17:11:53 +08:00
committed by GitHub
parent cc2df5e4ec
commit d62b310a21
33 changed files with 26143 additions and 137 deletions

View File

@@ -83,4 +83,6 @@ target_sources(${PROJECT_NAME}
ui/host_game_ui.cpp
ui/join_game_ui.cpp
ui/error_ui.cpp
localization.cpp
tools/system_locate.cpp
)

View File

@@ -3,10 +3,12 @@
#include "Cubed/camera.hpp"
#include "Cubed/config.hpp"
#include "Cubed/debug_collector.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/tools/arg_parser.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp"
#include "Cubed/tools/system_info.hpp"
#include "Cubed/tools/system_locate.hpp"
#include "Cubed/tools/text_tools.hpp"
#include "version.hpp"
@@ -39,6 +41,14 @@ void App::init(int argc, char** argv) {
handle_toml();
handle_argument(argc, argv);
auto locate = get_system_locale();
std::string default_value = "en_US";
if (locate.country == "CN") {
default_value = "zh_CN";
}
Localization::instance().load_language(
m_game_config.get("language", default_value));
m_window.init();
m_window.imgui_init();

61
src/localization.cpp Normal file
View File

@@ -0,0 +1,61 @@
#include "Cubed/localization.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp"
#include <fstream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
namespace fs = std::filesystem;
namespace Cubed {
Localization::Localization() {}
Localization& Localization::instance() {
static Localization s_instance;
return s_instance;
}
void Localization::load_language(std::string_view language) {
m_current_lang = language;
std::string path =
ASSETS_PATH + std::format("lang/{}.json", m_current_lang);
std::ifstream file(path);
if (!file.is_open()) {
Logger::error("Can't Open File {}", path);
ASSERT(false);
return;
}
try {
json j = json::parse(file);
m_translations.clear();
j.get_to(m_translations);
} catch (const json::parse_error& e) {
Logger::error("JSON syntax error: {}", e.what());
}
}
std::string_view Localization::lookup(const std::string& key) const {
auto it = m_translations.find(key);
if (it != m_translations.end()) {
return it->second;
}
Logger::error("Can't find key {} in language {}", key, m_current_lang);
return key;
}
bool Localization::has(const std::string& key) const {
return m_translations.contains(key);
}
void Localization::replace_all(std::string& str, std::string_view from,
std::string_view to) {
std::size_t pos = 0;
while ((pos = str.find(from, pos)) != std::string::npos) {
str.replace(pos, from.size(), to);
pos += to.size();
}
}
} // namespace Cubed

View File

@@ -187,7 +187,7 @@ void Renderer::render_lable(const Label& label) {
shader.set_loc("projection", m_ui_proj_matrix);
Font::text_texture()->bind(0);
Font::get().text_texture()->bind(0);
auto& data = label.data();
auto pos = label.pos();
auto& text_style = label.text_style();

View File

@@ -50,6 +50,7 @@ void Texture::tex_image_2d(TextureFormat internalformat, TextureFormat format,
GLenum type, const void* data, GLsizei width,
GLsizei height, GLint level, GLint border) {
bind();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
m_width = static_cast<float>(width);
m_height = static_cast<float>(height);
glTexImage2D(get_gl_texture_type(), level,
@@ -62,6 +63,7 @@ void Texture::tex_image_3d(TextureFormat internalformat, TextureFormat format,
GLsizei height, GLsizei depth, GLint level,
GLint border) {
bind();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
m_width = static_cast<float>(width);
m_height = static_cast<float>(height);
glTexImage3D(get_gl_texture_type(), level,
@@ -73,6 +75,7 @@ void Texture::tex_sub_image_3d(TextureFormat format, GLenum type,
GLint zoffset, GLsizei width, GLsizei height,
GLsizei depth, GLint level) const {
bind();
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexSubImage3D(get_gl_texture_type(), level, xoffset, yoffset, zoffset,
width, height, depth, std::to_underlying(format), type,
data);

View File

@@ -1,14 +1,12 @@
#include "Cubed/tools/font.hpp"
#include "Cubed/constants.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp"
namespace fs = std::filesystem;
namespace Cubed {
Font::Font() {
if (FT_Init_FreeType(&m_ft)) {
Logger::error("FREETYPE: Could not init FreeType Library");
}
@@ -17,54 +15,84 @@ Font::Font() {
}
FT_Set_Pixel_Sizes(m_face, 0, 48);
setup_font_character();
GLint max_layers;
glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &max_layers);
m_max_layers = std::min(max_layers, 2048);
Logger::info("Font Max Layers {}", m_max_layers);
m_hb_font = hb_ft_font_create_referenced(m_face);
m_text_texture = std::make_unique<Texture>(TextureType::TEXTURE_2D_ARRAY);
m_text_texture->tex_image_3d(TextureFormat::R8, TextureFormat::RED,
GL_UNSIGNED_BYTE, nullptr, CELL_SIZE,
CELL_SIZE, m_max_layers);
m_text_texture->set_linear();
m_text_texture->set_clamp_to_edge();
}
Font::~Font() {
hb_font_destroy(m_hb_font);
FT_Done_Face(m_face);
FT_Done_FreeType(m_ft);
}
void Font::load_character(char8_t c) {
if (FT_Load_Char(m_face, c, FT_LOAD_RENDER)) {
Logger::error("FREETYTPE: Failed to load Glyph");
return;
Glyph& Font::load_glyph(uint32_t glyph_index) {
auto it = m_cache.find(glyph_index);
if (it != m_cache.end()) {
return it->second;
}
if (FT_Load_Glyph(m_face, glyph_index, FT_LOAD_RENDER)) {
Logger::error("Failed to load glyph {}", glyph_index);
}
const auto& width = m_face->glyph->bitmap.width;
const auto& height = m_face->glyph->bitmap.rows;
m_text_texture->tex_sub_image_3d(TextureFormat::RED, GL_UNSIGNED_BYTE,
m_face->glyph->bitmap.buffer, 0, 0,
static_cast<int>(c), width, height);
Character character = {
glm::vec2{0.5f / m_texture_width, 0.5f / m_texture_height},
glm::vec2{(width - 0.5f) / m_texture_width,
(height - 0.5f) / m_texture_height},
glm::ivec2(m_face->glyph->bitmap.width, m_face->glyph->bitmap.rows),
glm::ivec2(m_face->glyph->bitmap_left, m_face->glyph->bitmap_top),
static_cast<GLuint>(m_face->glyph->advance.x)};
Glyph glyph;
m_characters.insert({c, std::move(character)});
glyph.size.x = m_face->glyph->bitmap.width;
glyph.size.y = m_face->glyph->bitmap.rows;
glyph.bearing.x = m_face->glyph->bitmap_left;
glyph.bearing.y = m_face->glyph->bitmap_top;
upload_glyph(glyph, m_face->glyph->bitmap.buffer);
auto [iter, _] = m_cache.try_emplace(glyph_index, std::move(glyph));
return iter->second;
}
void Font::setup_font_character() {
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
m_text_texture = std::make_unique<Texture>(TextureType::TEXTURE_2D_ARRAY);
m_text_texture->tex_image_3d(TextureFormat::R8, TextureFormat::RED,
GL_UNSIGNED_BYTE, nullptr, m_texture_width,
m_texture_height, MAX_CHARACTER);
void Font::upload_glyph(Glyph& glyph, const unsigned char* buffer) {
ASSERT(glyph.size.x <= CELL_SIZE);
ASSERT(glyph.size.y <= CELL_SIZE);
ASSERT(m_next_layer < m_max_layers);
for (char8_t c = 0; c < 128; c++) {
load_character(c);
}
m_text_texture->set_linear();
m_text_texture->set_clamp_to_edge(false, true, true);
m_text_texture->tex_sub_image_3d(RED, GL_UNSIGNED_BYTE, buffer, 0, 0,
m_next_layer, glyph.size.x, glyph.size.y);
glyph.layer = m_next_layer++;
glyph.uv_min = {0, 0};
glyph.uv_max = {glyph.size.x / float(CELL_SIZE),
glyph.size.y / float(CELL_SIZE)};
}
TextMesh Font::vertices(const std::string& text) {
Font& Font::get() {
static Font font;
return font;
}
TextMesh Font::vertices(const std::string& text) {
hb_buffer_t* buffer = hb_buffer_create();
hb_buffer_add_utf8(buffer, text.c_str(), text.size(), 0, text.size());
hb_buffer_guess_segment_properties(buffer);
hb_shape(m_hb_font, buffer, nullptr, 0);
unsigned int count;
hb_glyph_info_t* infos = hb_buffer_get_glyph_infos(buffer, &count);
hb_glyph_position_t* positions =
hb_buffer_get_glyph_positions(buffer, &count);
if (count == 0) {
hb_buffer_destroy(buffer);
return {};
}
std::vector<Vertex2D> vertices;
float min_x = std::numeric_limits<float>::max();
@@ -75,20 +103,18 @@ TextMesh Font::vertices(const std::string& text) {
float pen_x = 0.0f;
float pen_y = 0.0f;
for (char8_t c : text) {
auto it = font.m_characters.find(c);
if (it == font.m_characters.end()) {
Logger::error("Can't find character {}", static_cast<char>(c));
continue;
}
for (unsigned i = 0; i < count; i++) {
Character& ch = it->second;
uint32_t glyph_index = infos[i].codepoint;
float xpos = pen_x + ch.bearing.x;
float ypos = pen_y - ch.bearing.y;
Glyph& glyph = load_glyph(glyph_index);
float w = ch.size.x;
float h = ch.size.y;
float xpos = pen_x + positions[i].x_offset / 64.0f + glyph.bearing.x;
float ypos = pen_y - positions[i].y_offset / 64.0f - glyph.bearing.y;
float w = glyph.size.x;
float h = glyph.size.y;
min_x = std::min(min_x, xpos);
min_y = std::min(min_y, ypos);
@@ -96,28 +122,29 @@ TextMesh Font::vertices(const std::string& text) {
max_x = std::max(max_x, xpos + w);
max_y = std::max(max_y, ypos + h);
vertices.emplace_back(xpos, ypos + h, ch.uv_min.x, ch.uv_max.y,
static_cast<float>(c));
vertices.emplace_back(xpos, ypos, ch.uv_min.x, ch.uv_min.y,
static_cast<float>(c));
vertices.emplace_back(xpos + w, ypos, ch.uv_max.x, ch.uv_min.y,
static_cast<float>(c));
vertices.emplace_back(xpos, ypos + h, glyph.uv_min.x, glyph.uv_max.y,
static_cast<float>(glyph.layer));
vertices.emplace_back(xpos, ypos, glyph.uv_min.x, glyph.uv_min.y,
static_cast<float>(glyph.layer));
vertices.emplace_back(xpos + w, ypos, glyph.uv_max.x, glyph.uv_min.y,
static_cast<float>(glyph.layer));
vertices.emplace_back(xpos, ypos + h, ch.uv_min.x, ch.uv_max.y,
static_cast<float>(c));
vertices.emplace_back(xpos + w, ypos, ch.uv_max.x, ch.uv_min.y,
static_cast<float>(c));
vertices.emplace_back(xpos + w, ypos + h, ch.uv_max.x, ch.uv_max.y,
static_cast<float>(c));
vertices.emplace_back(xpos, ypos + h, glyph.uv_min.x, glyph.uv_max.y,
static_cast<float>(glyph.layer));
vertices.emplace_back(xpos + w, ypos, glyph.uv_max.x, glyph.uv_min.y,
static_cast<float>(glyph.layer));
vertices.emplace_back(xpos + w, ypos + h, glyph.uv_max.x,
glyph.uv_max.y, static_cast<float>(glyph.layer));
pen_x += (ch.advance >> 6);
pen_x += positions[i].x_advance / 64.0f;
pen_y += positions[i].y_advance / 64.0f;
}
// Top-left anchor point
for (auto& v : vertices) {
v.x -= min_x;
v.y -= min_y;
}
hb_buffer_destroy(buffer);
return {std::move(vertices), max_x - min_x, max_y - min_y, 0, 0};
}

View File

@@ -0,0 +1,75 @@
#include "Cubed/tools/system_locate.hpp"
#ifdef _WIN32
#define NOMINMAX
#include <Windows.h>
namespace Cubed {
SystemLocale get_system_locale() {
wchar_t locale_name[LOCALE_NAME_MAX_LENGTH]{};
if (GetUserDefaultLocaleName(locale_name, LOCALE_NAME_MAX_LENGTH) == 0)
return {};
char utf8[LOCALE_NAME_MAX_LENGTH * 4]{};
WideCharToMultiByte(CP_UTF8, 0, locale_name, -1, utf8, sizeof(utf8),
nullptr, nullptr);
SystemLocale result;
result.locale = utf8;
auto pos = result.locale.find('-');
if (pos != std::string::npos) {
result.country = result.locale.substr(pos + 1);
result.locale[pos] = '_'; // zh-CN -> zh_CN
}
return result;
}
} // namespace Cubed
#else
#include <cstdlib>
#include <string>
namespace Cubed {
SystemLocale get_system_locale() {
SystemLocale result;
const char* vars[] = {std::getenv("LC_ALL"), std::getenv("LC_MESSAGES"),
std::getenv("LANG")};
for (const char* s : vars) {
if (s && *s) {
result.locale = s;
break;
}
}
if (result.locale.empty())
return result;
auto dot = result.locale.find('.');
if (dot != std::string::npos)
result.locale.erase(dot);
auto at = result.locale.find('@');
if (at != std::string::npos)
result.locale.erase(at);
auto pos = result.locale.find('_');
if (pos != std::string::npos)
result.country = result.locale.substr(pos + 1);
return result;
}
} // namespace Cubed
#endif

View File

@@ -1,13 +1,23 @@
#include "Cubed/ui/combo_button.hpp"
#include "Cubed/localization.hpp"
namespace Cubed {
ComboButton::ComboButton(Widget* parent) : Button(parent) {}
Button& ComboButton::set_text(const std::string& text) {
m_text = text;
ComboButton& ComboButton::set_combo_text(const std::string& key,
const std::string& variable) {
m_key = key;
m_variable = variable;
update_text();
return *this;
}
Button& ComboButton::set_text(const std::string&) {
ASSERT_MSG(false,
"don't use this function, use, set_combo_button instead.");
return *this;
}
bool ComboButton::handle_mouse_button_event(const MouseButtonEvent& e) {
if (e.action == KeyAction::PRESS && e.key == MouseKey::LEFT_BUTTON) {
if (m_hovered && m_enable) {
@@ -47,8 +57,7 @@ void ComboButton::update_text() {
if (m_suffix.empty()) {
return;
}
auto text = m_text + ": " + m_suffix[m_index];
m_foreground->set_text(text);
m_foreground->set_text(tr(m_key, arg(m_variable, m_suffix[m_index])));
update_text_scale();
}

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/credits_ui.hpp"
#include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/scene/credits_scene.hpp"
#include "Cubed/scene/scene_manager.hpp"
#include "Cubed/ui/button.hpp"
@@ -30,7 +31,7 @@ void CreditsUI::init() {
"texture/ui/button001.png",
m_scene.scene_manager().app().texture_manager());
button.set_text("Return");
button.set_text(tr("button.return"));
button.set_anchor(Anchor::BOTTOM_CENTER).set_offset({0, -20});
button.set_clicked([this]() { m_scene.scene_manager().request_pop(); });
}
@@ -59,6 +60,8 @@ void CreditsUI::init() {
add_text("zstd");
add_text("OpenAl Soft");
add_text("dr_libs");
add_text("nlohmann/json");
add_text("HarfBuzz");
add_text("Music", 0.8f);
add_text("'Find a Peaceful Place' by ROZKOL (Free Music Archive), CC "
"BY 4.0.");

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/error_ui.hpp"
#include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/scene/world_scene.hpp"
#include "Cubed/ui/column_layout.hpp"
#include "Cubed/ui/image.hpp"
@@ -38,7 +39,7 @@ void ErrorUI::init() {
{
auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager);
button.set_text("Return");
button.set_text(tr("button.return"));
button.set_clicked([this, &button]() {
button.set_enable(false);
m_scene.scene_manager().request_pop();

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/host_game_ui.hpp"
#include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/scene/host_game_scene.hpp"
#include "Cubed/scene/scene_manager.hpp"
#include "Cubed/ui/button.hpp"
@@ -32,7 +33,7 @@ void HostGameUI::init() {
param.host_game = true;
{
auto& label = layout.add_child<Label>();
label.set_text("Create A New World");
label.set_text(tr("hostgame.create_a_new_world"));
label.set_scale(0.7f);
}
{
@@ -45,7 +46,7 @@ void HostGameUI::init() {
}
{
auto& text_seed = layout.add_child<TextField>();
text_seed.set_show_text("WorldSeed");
text_seed.set_show_text(tr("hostgame.world_seed"));
text_seed.set_app(&m_scene.scene_manager().app());
text_seed.set_default_image(texture_manager);
text_seed.set_on_finish([this, &text_seed]() {
@@ -67,7 +68,7 @@ void HostGameUI::init() {
{
auto& text_port = layout.add_child<TextField>();
text_port.set_default_image(texture_manager);
text_port.set_show_text("Port: 25530");
text_port.set_show_text(tr("hostgame.port"));
text_port.set_app(&m_scene.scene_manager().app());
text_port.set_on_finish([this, &text_port]() {
int port = 25530;
@@ -93,7 +94,7 @@ void HostGameUI::init() {
{
auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager);
button.set_text("Start Game");
button.set_text(tr("hostgame.create_world"));
button.set_clicked([this, &button]() {
button.set_enable(false);
m_scene.scene_manager().request_change(SceneType::WORLD);
@@ -102,7 +103,7 @@ void HostGameUI::init() {
{
auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager);
button.set_text("Return");
button.set_text(tr("button.return"));
button.set_clicked([this, &button]() {
button.set_enable(false);
m_scene.scene_manager().request_pop();

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/join_game_ui.hpp"
#include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/scene/join_game_scene.hpp"
#include "Cubed/ui/button.hpp"
#include "Cubed/ui/column_layout.hpp"
@@ -30,7 +31,7 @@ void JoinGameUI::init() {
param.host_game = false;
{
auto& label = layout.add_child<Label>();
label.set_text("Join A World");
label.set_text(tr("joingame.join_a_world"));
label.set_scale(0.7f);
}
{
@@ -43,7 +44,7 @@ void JoinGameUI::init() {
}
{
auto& text_ip = layout.add_child<TextField>();
text_ip.set_show_text("Server Ip");
text_ip.set_show_text(tr("joingame.server_ip"));
text_ip.set_default_image(texture_manager);
text_ip.set_app(&m_scene.scene_manager().app());
text_ip.set_on_finish([this, &text_ip]() {
@@ -96,7 +97,7 @@ void JoinGameUI::init() {
{
auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager);
button.set_text("Join Game");
button.set_text(tr("joingame.join_world"));
button.set_clicked([this, &button]() {
button.set_enable(false);
m_scene.scene_manager().request_change(SceneType::WORLD);
@@ -105,7 +106,7 @@ void JoinGameUI::init() {
{
auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager);
button.set_text("Return");
button.set_text(tr("button.return"));
button.set_clicked([this, &button]() {
button.set_enable(false);
m_scene.scene_manager().request_pop();

View File

@@ -25,7 +25,7 @@ void Label::on_update(float dt) { (void)dt; }
void Label::on_render(Renderer& renderer) { renderer.render_lable(*this); }
void Label::update_vertices() {
auto textmesh = Font::vertices(m_text.text);
auto textmesh = Font::get().vertices(m_text.text);
m_data.m_vertices = std::move(textmesh.vertices);
m_offset_x = textmesh.min_x;

View File

@@ -1,6 +1,7 @@
#include "Cubed/ui/main_menu_ui_manager.hpp"
#include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/render/renderer.hpp"
#include "Cubed/scene/main_menu_scene.hpp"
#include "Cubed/scene/scene_manager.hpp"
@@ -32,7 +33,7 @@ void MainMenuUIManager::init() {
start_game_button.set_background_image("texture/ui/button001.png",
texture_manager);
start_game_button.set_text("Host Game");
start_game_button.set_text(tr("menu.main.host_game"));
start_game_button.set_clicked([this, &start_game_button]() {
start_game_button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::HOST_GAME);
@@ -44,7 +45,7 @@ void MainMenuUIManager::init() {
start_game_button.set_background_image("texture/ui/button001.png",
texture_manager);
start_game_button.set_text("Join Game");
start_game_button.set_text(tr("menu.main.join_game"));
start_game_button.set_clicked([this, &start_game_button]() {
start_game_button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::JOIN_GAME);
@@ -54,7 +55,7 @@ void MainMenuUIManager::init() {
{
auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager);
button.set_text("Settings");
button.set_text(tr("menu.settings"));
button.set_clicked([this, &button]() {
button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::SETTINGS);
@@ -67,7 +68,7 @@ void MainMenuUIManager::init() {
button.set_background_image("texture/ui/button001.png",
texture_manager);
button.set_text("Credits");
button.set_text(tr("menu.main.credits"));
button.set_clicked([this, &button]() {
button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::CREDITS);
@@ -78,7 +79,7 @@ void MainMenuUIManager::init() {
auto& exit_game = layout.add_child<Button>();
exit_game.set_background_image("texture/ui/button001.png",
texture_manager);
exit_game.set_text("Quit");
exit_game.set_text(tr("menu.main.quit"));
exit_game.set_clicked([this]() {
m_scene.scene_manager().app().window().should_close_window();
@@ -94,18 +95,21 @@ void MainMenuUIManager::init() {
info_layout.set_child_anchor(ColumnLayoutAnchor::LEFT);
auto& player_name = info_layout.add_child<Label>();
player_name.set_scale(SCALE);
player_name.set_text(std::format(
"Player: {}", m_scene.scene_manager().app().argument().player));
player_name.set_text(
tr("menu.main.player_name",
arg("name", m_scene.scene_manager().app().argument().player)));
auto& version = info_layout.add_child<Label>();
version.set_scale(SCALE);
std::string version_str = "Cubed: " CUBED_VERSION;
std::string version_str = CUBED_VERSION;
#ifdef DEBUG_MODE
version_str.append("-debug");
#else
version_str.append("-release");
#endif
version.set_text(version_str);
version.set_text(
tr("menu.main.cubed_version", arg("version", version_str)));
}
m_root_widget = std::move(image);

View File

@@ -1,4 +1,5 @@
#include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/render/renderer.hpp"
#include "Cubed/scene/scene_manager.hpp"
#include "Cubed/scene/world_scene.hpp"
@@ -23,20 +24,20 @@ void PauseMenuUIManager::init() {
layout.set_spacing(20);
{
auto& title = layout.add_child<Label>();
title.set_text("Pause Menu");
title.set_text(tr("menu.pause.pause_menu"));
title.set_scale(0.75f);
}
{
auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager);
button.set_text("Back to Game");
button.set_text(tr("menu.pause.back_to_game"));
button.set_clicked([this]() { m_scene.set_pause(false); });
}
{
auto& button = layout.add_child<Button>();
button.set_default_image(texture_manager);
button.set_text("Settings");
button.set_text(tr("menu.settings"));
button.set_clicked([this, &button]() {
button.set_enable(false);
m_scene.scene_manager().request_push(SceneType::SETTINGS);
@@ -49,7 +50,7 @@ void PauseMenuUIManager::init() {
back_main.set_background_image("texture/ui/button001.png",
texture_manager);
back_main.set_text("Return to Menu");
back_main.set_text(tr("menu.pause.return_to_main_menu"));
back_main.set_clicked([this, &back_main]() {
back_main.set_enable(false);
m_scene.scene_manager().request_pop();

View File

@@ -1,9 +1,11 @@
#include "Cubed/ui/settings_ui.hpp"
#include "Cubed/app.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/render/renderer.hpp"
#include "Cubed/scene/scene_manager.hpp"
#include "Cubed/scene/settings_scene.hpp"
#include "Cubed/tools/system_locate.hpp"
#include "Cubed/ui/button.hpp"
#include "Cubed/ui/column_layout.hpp"
#include "Cubed/ui/combo_button.hpp"
@@ -39,33 +41,34 @@ void SettingsUI::init() {
};
{
auto& title = layout.add_child<Label>();
title.set_text("Setting");
title.set_text(tr("settings.title"));
}
{
auto& fov = layout.add_child<Slider>();
fov.set_slider(&v.fov, 20.0f, 120.0f);
set_default_slider_image(fov);
fov.set_text("Fov");
fov.set_slider_text("settings.fov", "fov");
auto& sensitivity = layout.add_child<Slider>();
sensitivity.set_slider(&v.mouse_sensitivity, 0.01f, 1.0f);
set_default_slider_image(sensitivity);
sensitivity.set_text("Sensitivity");
sensitivity.set_slider_text("settings.mouse_sensitivity",
"sensitivity");
auto& distance = layout.add_child<Slider>();
distance.set_slider(&v.rendering_distance, 2, 64);
set_default_slider_image(distance);
distance.set_text("Distance");
distance.set_slider_text("settings.distance", "distance");
auto& music = layout.add_child<Slider>();
music.set_slider(&v.music, 0.0f, 1.0f);
set_default_slider_image(music);
music.set_text("Music");
music.set_slider_text("settings.music_volume", "music");
auto& sfx = layout.add_child<Slider>();
sfx.set_slider(&v.sfx, 0.0f, 1.0f);
set_default_slider_image(sfx);
sfx.set_text("SFX");
sfx.set_slider_text("settings.sfx_volume", "sfx");
}
auto& config = m_scene.scene_manager().app().config();
{
@@ -73,13 +76,13 @@ void SettingsUI::init() {
bool full = config.get("window.fullscreen", false);
auto& fullscreen = layout.add_child<ComboButton>();
fullscreen.set_index(!full ? 0 : 1);
fullscreen.set_text("Fullscreen");
fullscreen.set_combo_text("settings.fullscreen", "state");
std::vector<std::pair<std::string, std::function<void()>>> comb;
comb.emplace_back("off", [&config, this]() {
comb.emplace_back(tr("common.off"), [&config, this]() {
config.set("window.fullscreen", false);
m_scene.scene_manager().app().window().reload_config();
});
comb.emplace_back("on", [&config, this]() {
comb.emplace_back(tr("common.on"), [&config, this]() {
config.set("window.fullscreen", true);
m_scene.scene_manager().app().window().reload_config();
});
@@ -91,13 +94,13 @@ void SettingsUI::init() {
bool enable_vsync = config.get("window.V-Sync", true);
auto& vsync = layout.add_child<ComboButton>();
vsync.set_default_image(texture_manager);
vsync.set_text("V-Sync");
vsync.set_combo_text("settings.vsync", "state");
std::vector<ComboPair> combos;
combos.emplace_back("off", [this, &config]() {
combos.emplace_back(tr("common.off"), [this, &config]() {
config.set("window.V-Sync", false);
m_scene.scene_manager().app().window().reload_config();
});
combos.emplace_back("on", [this, &config]() {
combos.emplace_back(tr("common.on"), [this, &config]() {
config.set("window.V-Sync", true);
m_scene.scene_manager().app().window().reload_config();
});
@@ -124,12 +127,12 @@ void SettingsUI::init() {
auto& aniso_button = layout.add_child<ComboButton>();
aniso_button.set_default_image(texture_manager);
aniso_button.set_text("Aniso");
aniso_button.set_combo_text("settings.aniso", "aniso");
aniso_button.set_index(cur_index);
std::vector<ComboPair> combos;
auto get_suffix = [](int i) -> std::pair<std::string, int> {
if (i == 0) {
return {"off", 1};
return {tr("common.off"), 1};
}
int n = 1;
for (int j = 0; j < i; j++) {
@@ -147,11 +150,47 @@ void SettingsUI::init() {
}
aniso_button.set_combos(combos);
}
{
auto& lang_button = layout.add_child<ComboButton>();
auto& label = layout.add_child<Label>();
label.set_text(tr("settings.restart_game_info"));
label.set_scale(0.7f);
label.set_color(Color::RED);
label.set_visible(false);
lang_button.set_default_image(texture_manager);
lang_button.set_combo_text("settings.language", "lang");
auto locate = get_system_locale();
std::string default_value = "en_US";
if (locate.country == "CN") {
default_value = "zh_CN";
}
auto lang = config.get("language", default_value);
int index = 0;
if (lang == "en_US") {
index = 0;
} else if (lang == "zh_CN") {
index = 1;
}
lang_button.set_index(index);
std::vector<ComboPair> combos;
combos.emplace_back("English", [&label, &config]() {
config.set("language", std::string("en_US"));
label.set_visible(true);
});
combos.emplace_back("简体中文", [&label, &config]() {
config.set("language", std::string("zh_CN"));
label.set_visible(true);
});
lang_button.set_combos(combos);
}
auto& return_button = layout.add_child<Button>();
return_button.set_background_image("texture/ui/button001.png",
texture_manager);
return_button.set_text("Save and Return");
return_button.set_text(tr("button.done"));
return_button.set_clicked(
[this]() { m_scene.scene_manager().request_pop(); });

View File

@@ -1,5 +1,6 @@
#include "Cubed/ui/slider.hpp"
#include "Cubed/localization.hpp"
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp"
@@ -84,8 +85,10 @@ Slider& Slider::set_height(float h) {
update_text_scale();
return *this;
}
Slider& Slider::set_text(const std::string& text) {
m_text = text;
Slider& Slider::set_slider_text(const std::string& key,
const std::string& variable) {
m_key = key;
m_variable = variable;
return *this;
}
Slider& Slider::set_track_image(const std::string& path,
@@ -164,7 +167,7 @@ void Slider::on_update(float dt) {
}
float range = m_max - m_min;
if (range <= 0.0f) {
Logger::error("Slider {} Range {} is <= 0.0f", m_text, range);
Logger::error("Slider {} Range {} is <= 0.0f", m_key, range);
ASSERT(false);
return;
}
@@ -184,11 +187,12 @@ void Slider::on_update(float dt) {
m_thumb->set_offset({offset, 0});
if (m_label) {
if (m_type == ValueType::FLOAT && m_float_value) {
m_label->set_text(
std::format("{}: {:.2f}", m_text, *m_float_value));
m_label->set_text(tr(
m_key, arg(m_variable, std::format("{:.2f}", *m_float_value))));
}
if (m_type == ValueType::INT && m_int_value) {
m_label->set_text(std::format("{}: {}", m_text, *m_int_value));
m_label->set_text(tr(m_key, arg(m_variable, *m_int_value)));
}
}
if (m_track) {