Pārlūkot izejas kodu

:sparkles: added translations for German

Felix Bytow 1 gadu atpakaļ
vecāks
revīzija
442a31062d

+ 1 - 0
CMakeLists.txt

@@ -56,6 +56,7 @@ add_executable(Snake WIN32 MACOSX_BUNDLE
     game/ui/UiColor.hxx game/ui/UiColor.cxx
     game/HighScoreManager.cxx game/HighScoreManager.hxx
     ${CMAKE_CURRENT_BINARY_DIR}/Config.hxx
+    game/TranslationManager.cxx game/TranslationManager.hxx
     NonCopyable.hxx
 )
 

+ 26 - 0
assets/translations.ini

@@ -0,0 +1,26 @@
+[New game]
+de = Neues Spiel
+
+[Continue]
+de = Fortsetzen
+
+[High Scores]
+de = Bestenliste
+
+[Quit]
+de = Beenden
+
+[Score]
+de = Punkte
+
+[Frames per second]
+de = Bilder pro Sekunde
+
+[Congratulations, you made it to the top 10!]
+de = Glückwunsch, du bist unter den Top 10!
+
+[You reached {} points!]
+de = Du hast {} Punkte erreicht!
+
+[Please enter your name:]
+de = Bitte gib deinen Namen ein:

+ 5 - 0
game/AssetManager.cxx

@@ -1,5 +1,6 @@
 #include "AssetManager.hxx"
 #include "Config.hxx"
+#include "TranslationManager.hxx"
 
 #include <algorithm>
 #include <array>
@@ -83,6 +84,10 @@ void AssetManager::load_assets(std::filesystem::path const& asset_directory)
       font_assets_[filename] = font;
       SDL_Log("Loaded font %s successfully.", filename.c_str());
     }
+    else if (ext==".ini") {
+      TranslationManager::instance().load(path);
+      SDL_Log("Loaded translations from %s.", filename.c_str());
+    }
     ++assets_loaded_;
   }
 }

+ 16 - 6
game/GameOverState.cxx

@@ -1,9 +1,15 @@
 #include "GameOverState.hxx"
 #include "GameStateManager.hxx"
-
+#include "TranslationManager.hxx"
 #include "HighScoreManager.hxx"
 #include "../SDLRenderer.hxx"
 
+#include <regex>
+
+namespace {
+  std::regex const PLACEHOLDER_REGEX{R"(\{\})"};
+}
+
 GameOverState::GameOverState()
     :font_{"kenney_pixel.ttf"}
 {
@@ -13,6 +19,7 @@ void GameOverState::on_enter(GameStateManager& gsm)
 {
   SDL_ShowCursor(SDL_ENABLE);
 
+  ok_button_.set_title(TranslationManager::instance().get_translation("OK"));
   if (HighScoreManager::instance().has_new_score()) {
     name_input_.set_focus(true);
     name_input_.set_value("Anon");
@@ -62,6 +69,8 @@ void GameOverState::update(GameStateManager& gsm, std::chrono::milliseconds cons
 
 void GameOverState::render(SDLRenderer& renderer)
 {
+  auto const& tm = TranslationManager::instance();
+
   int width, height;
   SDL_GetRendererOutputSize(renderer, &width, &height);
 
@@ -71,10 +80,10 @@ void GameOverState::render(SDLRenderer& renderer)
   int base_y;
   auto const& hsm = HighScoreManager::instance();
   if (hsm.has_new_score()) {
-    auto const score_text = "Congratulations, you made it to the top 10!\nYou reached "
-        +std::to_string(hsm.get_new_score())
-        +" points!\nPlease enter your name:";
-    SDL_Surface* text_surface = TTF_RenderText_Solid_Wrapped(font_, score_text.c_str(),
+    auto score_text = (tm.get_translation("Congratulations, you made it to the top 10!")
+        +"\n"+tm.get_translation("You reached {} points!")+"\n"+tm.get_translation("Please enter your name:"));
+    score_text = std::regex_replace(score_text, PLACEHOLDER_REGEX, std::to_string(hsm.get_new_score()));
+    SDL_Surface* text_surface = TTF_RenderUTF8_Solid_Wrapped(font_, score_text.c_str(),
         {255, 255, 255, SDL_ALPHA_OPAQUE}, 0);
     SDL_Texture* text = SDL_CreateTextureFromSurface(renderer, text_surface);
     SDL_FreeSurface(text_surface);
@@ -95,7 +104,8 @@ void GameOverState::render(SDLRenderer& renderer)
     base_y += name_input_.get_bounding_box().h+10;
   }
   else {
-    SDL_Surface* text_surface = TTF_RenderText_Solid(font_, "Game over!", {255, 255, 255, SDL_ALPHA_OPAQUE});
+    SDL_Surface* text_surface = TTF_RenderUTF8_Solid(font_, tm.get_translation("Game over!").c_str(),
+        {255, 255, 255, SDL_ALPHA_OPAQUE});
     SDL_Texture* text = SDL_CreateTextureFromSurface(renderer, text_surface);
     SDL_FreeSurface(text_surface);
     int text_width, text_height;

+ 1 - 1
game/GameOverState.hxx

@@ -23,7 +23,7 @@ public:
   void render(SDLRenderer& renderer) override;
 
 private:
-  Button ok_button_{"OK", 0, 0, 800, 80, UiColor::Green};
+  Button ok_button_{0, 0, 800, 80, UiColor::Green};
   LineInput name_input_{0, 0, 800, 80};
 
   Asset<TTF_Font*> font_;

+ 4 - 3
game/HighScoreState.cxx

@@ -2,6 +2,7 @@
 
 #include "HighScoreManager.hxx"
 #include "GameStateManager.hxx"
+#include "TranslationManager.hxx"
 #include "../SDLRenderer.hxx"
 
 HighScoreState::HighScoreState()
@@ -43,7 +44,7 @@ void HighScoreState::render(SDLRenderer& renderer)
   auto const scores = HighScoreManager::instance().get_scores();
   for (auto const& score: scores) {
     std::string text = score.player_name_+": "+std::to_string(score.points_);
-    SDL_Surface* surface = TTF_RenderText_Solid(font, text.c_str(), color);
+    SDL_Surface* surface = TTF_RenderUTF8_Solid(font, text.c_str(), color);
     SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
 
     SDL_Rect textRect;
@@ -65,8 +66,8 @@ void HighScoreState::render(SDLRenderer& renderer)
 
 void HighScoreState::render_heading(SDLRenderer& renderer, int const width, SDL_Color const& color, int& current_height)
 {
-  std::string heading = "High Scores";
-  SDL_Surface* headingSurface = TTF_RenderText_Solid(font_, "High Scores", color);
+  SDL_Surface* headingSurface = TTF_RenderUTF8_Solid(font_,
+      TranslationManager::instance().get_translation("High Scores").c_str(), color);
   SDL_Texture* headingTexture = SDL_CreateTextureFromSurface(renderer, headingSurface);
 
   SDL_Rect heading_rect;

+ 8 - 0
game/MenuState.cxx

@@ -1,6 +1,7 @@
 #include "MenuState.hxx"
 #include "PlayingState.hxx"
 #include "GameStateManager.hxx"
+#include "TranslationManager.hxx"
 #include "../SDLRenderer.hxx"
 
 #include <array>
@@ -9,6 +10,13 @@ void MenuState::on_enter(GameStateManager& gsm)
 {
   active_button_ = 0;
 
+  auto const& tm = TranslationManager::instance();
+  new_game_button_.set_title(tm.get_translation("New game"));
+  continue_button_.set_title(tm.get_translation("Continue"));
+  high_score_button_.set_title(tm.get_translation("High Scores"));
+  credits_button_.set_title(tm.get_translation("Credits"));
+  quit_button_.set_title(tm.get_translation("Quit"));
+
   game_.reset();
   if (auto const parent = gsm.parent(); parent!=nullptr) {
     continue_button_.set_visible(true);

+ 5 - 5
game/MenuState.hxx

@@ -28,11 +28,11 @@ private:
   static int constexpr BUTTON_HEIGHT = 80;
   static int constexpr BUTTON_WIDTH = 350;
 
-  Button new_game_button_{"New game", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, UiColor::Green};
-  Button continue_button_{"Continue", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, UiColor::Blue};
-  Button high_score_button_{"High Scores", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT};
-  Button credits_button_{"Credits", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT};
-  Button quit_button_{"Quit", 0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, UiColor::Red};
+  Button new_game_button_{0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, UiColor::Green};
+  Button continue_button_{0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, UiColor::Blue};
+  Button high_score_button_{0, 0, BUTTON_WIDTH, BUTTON_HEIGHT};
+  Button credits_button_{0, 0, BUTTON_WIDTH, BUTTON_HEIGHT};
+  Button quit_button_{0, 0, BUTTON_WIDTH, BUTTON_HEIGHT, UiColor::Red};
 
   std::optional<PlayingState*> game_{};
 

+ 7 - 4
game/PlayingState.cxx

@@ -2,6 +2,7 @@
 
 #include "HighScoreManager.hxx"
 #include "GameStateManager.hxx"
+#include "TranslationManager.hxx"
 
 #include <algorithm>
 #include <cfenv>
@@ -167,8 +168,10 @@ void PlayingState::render(SDLRenderer& renderer)
 
 void PlayingState::render_ui(SDLRenderer& renderer, SDL_Rect const& playing_field)
 {
-  auto const score_text = "Score: "+std::to_string(length_);
-  SDL_Surface* text_surface = TTF_RenderText_Solid(font_, score_text.c_str(), {255, 255, 255, SDL_ALPHA_OPAQUE});
+  auto const& tm = TranslationManager::instance();
+
+  auto const score_text = tm.get_translation("Score")+": "+std::to_string(length_);
+  SDL_Surface* text_surface = TTF_RenderUTF8_Solid(font_, score_text.c_str(), {255, 255, 255, SDL_ALPHA_OPAQUE});
   SDL_Texture* text = SDL_CreateTextureFromSurface(renderer, text_surface);
   SDL_FreeSurface(text_surface);
   int text_width, text_height;
@@ -177,8 +180,8 @@ void PlayingState::render_ui(SDLRenderer& renderer, SDL_Rect const& playing_fiel
   SDL_RenderCopy(renderer, text, nullptr, &render_quad);
   SDL_DestroyTexture(text);
 
-  auto const fps_text = "Frames per second: "+std::to_string(fps_);
-  text_surface = TTF_RenderText_Solid(font_, fps_text.c_str(), {255, 255, 255, SDL_ALPHA_OPAQUE});
+  auto const fps_text = tm.get_translation("Frames per second")+": "+std::to_string(fps_);
+  text_surface = TTF_RenderUTF8_Solid(font_, fps_text.c_str(), {255, 255, 255, SDL_ALPHA_OPAQUE});
   text = SDL_CreateTextureFromSurface(renderer, text_surface);
   SDL_FreeSurface(text_surface);
   SDL_QueryTexture(text, nullptr, nullptr, &text_width, &text_height);

+ 90 - 0
game/TranslationManager.cxx

@@ -0,0 +1,90 @@
+#include "TranslationManager.hxx"
+
+#include <SDL.h>
+
+#include <fstream>
+#include <optional>
+#include <regex>
+
+namespace {
+  std::optional<SupportedLanguage> language_from_string(std::string const& lang)
+  {
+    static std::unordered_map<std::string, SupportedLanguage> const MAPPING{
+        {"en", SupportedLanguage::English},
+        {"de", SupportedLanguage::German},
+    };
+
+    auto const it = MAPPING.find(lang);
+    if (it==MAPPING.end())
+      return {};
+    return it->second;
+  }
+
+  SupportedLanguage get_preferred_language()
+  {
+    auto const locales = SDL_GetPreferredLocales();
+    for (auto const* locale = locales; locale->language!=nullptr; ++locale) {
+      auto const lang = language_from_string(locale->language);
+      if (lang.has_value()) {
+        SDL_free(locales);
+        return lang.value();
+      }
+    }
+    SDL_free(locales);
+    return SupportedLanguage::English;
+  }
+
+  std::regex const KEY_REGEX = std::regex{R"(^\[([^\]]*)\]$)"};
+  std::regex const VALUE_REGEX = std::regex{R"(^(en|de) = (.+)$)"};
+}
+
+TranslationManager::TranslationManager() noexcept
+    :current_language_{::get_preferred_language()}
+{
+}
+
+TranslationManager& TranslationManager::instance()
+{
+  static TranslationManager tm;
+  return tm;
+}
+
+std::string TranslationManager::get_translation(std::string const& key) const
+{
+  auto const it = translations_.find(key);
+  if (it==translations_.end())
+    return key;
+
+  auto const lang_idx = static_cast<std::size_t>(current_language_);
+  if (it->second[lang_idx].empty())
+    return key;
+
+  return it->second[lang_idx];
+}
+
+void TranslationManager::load(std::filesystem::path const& filename)
+{
+  std::ifstream input{filename};
+
+  std::string current_key{};
+  for (std::string line; std::getline(input, line);) {
+    if (std::smatch key_match; std::regex_match(line, key_match, ::KEY_REGEX)) {
+      current_key = key_match.str(1);
+    }
+    else if (std::smatch value_match; std::regex_match(line, value_match, ::VALUE_REGEX)) {
+      // hacky, but right now the regex already makes sure we alway get a language back
+      auto const lang = ::language_from_string(value_match.str(1)).value();
+      auto const value = value_match.str(2);
+
+      if (current_key.empty()) {
+        SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Translations without key not loaded! (%s)", value.c_str());
+      }
+      else {
+        translations_[current_key][static_cast<std::size_t>(lang)] = value;
+      }
+    }
+    else if (!line.empty()) {
+      SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION, "Translation line with unsupported format: %s", line.c_str());
+    }
+  }
+}

+ 37 - 0
game/TranslationManager.hxx

@@ -0,0 +1,37 @@
+#pragma once
+
+#ifndef SNAKE_TRANSLATIONMANAGER_HXX
+#define SNAKE_TRANSLATIONMANAGER_HXX
+
+#include <array>
+#include <filesystem>
+#include <string>
+#include <unordered_map>
+
+enum class SupportedLanguage {
+  English = 0,
+  German,
+
+  NUM_SUPPORTED_LANGUAGES,
+};
+
+class TranslationManager final {
+public:
+  static TranslationManager& instance();
+
+  void load(std::filesystem::path const& filename);
+
+  [[nodiscard]] std::string get_translation(std::string const& key) const;
+
+private:
+  TranslationManager() noexcept;
+
+  SupportedLanguage current_language_;
+
+  std::unordered_map<
+      std::string,
+      std::array<std::string, static_cast<std::size_t>(SupportedLanguage::NUM_SUPPORTED_LANGUAGES)>
+  > translations_;
+};
+
+#endif // SNAKE_TRANSLATIONMANAGER_HXX

+ 12 - 2
game/ui/Button.cxx

@@ -56,8 +56,8 @@ namespace {
   }
 }
 
-Button::Button(std::string title, int const x, int const y, int const w, int const h, UiColor const color)
-    :title_{std::move(title)}, x_{x}, y_{y}, w_{w}, h_{h}, pressed_{false},
+Button::Button(int const x, int const y, int const w, int const h, UiColor const color)
+    :x_{x}, y_{y}, w_{w}, h_{h}, pressed_{false},
      visible_{true},
      up_{ui_image("button_up", color)},
      down_{ui_image("button_down", color)},
@@ -69,6 +69,16 @@ Button::Button(std::string title, int const x, int const y, int const w, int con
   on_click_ = [] { };
 }
 
+void Button::set_title(std::string const& title)
+{
+  title_ = title;
+}
+
+std::string const& Button::title() const
+{
+  return title_;
+}
+
 void Button::set_pressed(bool const pressed)
 {
   pressed_ = pressed;

+ 6 - 2
game/ui/Button.hxx

@@ -19,7 +19,11 @@ public:
   static int constexpr MIN_WIDTH = 12;
   static int constexpr MIN_HEIGHT = 14;
 
-  Button(std::string title, int x, int y, int w, int h, UiColor color = UiColor::Grey);
+  Button(int x, int y, int w, int h, UiColor color = UiColor::Grey);
+
+  void set_title(std::string const& title);
+
+  [[nodiscard]] std::string const& title() const;
 
   void set_pressed(bool pressed);
 
@@ -44,7 +48,7 @@ public:
   [[nodiscard]] SDL_Rect get_bounding_box() const;
 
 private:
-  std::string title_;
+  std::string title_{};
   int x_, y_, w_, h_;
   bool pressed_;
   bool visible_;