Tutorial 98: Localization and Multiple Languages
String table approach (JSON per language)
Store all UI strings in JSON files, one per language. Load the correct file at startup based on the detected locale.
// assets/lang/en.json
{
"menu.play": "Play",
"menu.options": "Options",
"menu.quit": "Quit",
"hud.score": "Score: {0}",
"hud.lives": "Lives: {0}"
}
// assets/lang/cs.json
{
"menu.play": "Hrát",
"menu.options": "Možnosti",
"menu.quit": "Konec",
"hud.score": "Skóre: {0}",
"hud.lives": "Životy: {0}"
}
LocalizationManager class with JSON loading
#include "Microsoft/Xna/Framework/Game.hpp"
#include <string>
#include <unordered_map>
#include <fstream>
#include <sstream>
#include <stdexcept>
// Minimal JSON parser for flat string->string objects
// (In production, use nlohmann/json or rapidjson)
class LocalizationManager {
public:
void Load(const std::string& langCode) {
strings_.clear();
currentLang_ = langCode;
std::string path = "assets/lang/" + langCode + ".json";
std::ifstream f(path);
if (!f.is_open())
throw std::runtime_error("Language file not found: " + path);
std::string line;
while (std::getline(f, line)) {
// Very simple key-value extraction:
// "key": "value"
auto ks = line.find('"');
if (ks == std::string::npos) continue;
auto ke = line.find('"', ks + 1);
auto vs = line.find('"', ke + 2);
auto ve = line.rfind('"');
if (ks == std::string::npos || vs == std::string::npos || vs == ve) continue;
std::string key = line.substr(ks+1, ke-ks-1);
std::string val = line.substr(vs+1, ve-vs-1);
strings_[key] = val;
}
}
// Look up a string by key
const std::string& Get(const std::string& key) const {
auto it = strings_.find(key);
if (it == strings_.end()) return key; // fallback: return key itself
return it->second;
}
// Format: replace {0}, {1} with args
std::string Format(const std::string& key,
std::initializer_list<std::string> args) const {
std::string result = Get(key);
int i = 0;
for (const auto& arg : args) {
std::string placeholder = "{" + std::to_string(i++) + "}";
size_t pos;
while ((pos = result.find(placeholder)) != std::string::npos)
result.replace(pos, placeholder.size(), arg);
}
return result;
}
const std::string& CurrentLang() const { return currentLang_; }
private:
std::unordered_map<std::string, std::string> strings_;
std::string currentLang_;
};
// Global L("key") helper -- set g_loc before calling
static LocalizationManager* g_loc = nullptr;
inline const std::string& L(const std::string& key) {
return g_loc ? g_loc->Get(key) : key;
}
SpriteFont Unicode support
CNA's SpriteFont loads a JSON descriptor that specifies which Unicode code point
ranges to rasterize. For Latin scripts, the default range (U+0020–U+00FF) is sufficient.
For Czech/Slovak add U+0100–U+017F. For Cyrillic add U+0400–U+04FF. For CJK you
need a large range (U+4E00–U+9FFF) and a matching font file.
// assets/fonts/ui_font.json
{
"fontFile": "fonts/NotoSans-Regular.ttf",
"size": 18,
"characterRanges": [
[32, 255],
[256, 383]
]
}
CNA font.json glyph ranges
// Load a localization-aware SpriteFont:
spriteFont_ = std::make_unique<SpriteFont>(
"assets/fonts/ui_font.json",
getGraphicsDeviceProperty());
// Draw localized string:
spriteBatch_->DrawString(*spriteFont_,
L("menu.play"),
Vector2(100, 200),
Color::White);
Right-to-left text challenges
RTL languages (Arabic, Hebrew) require bidirectional text layout (Unicode BiDi algorithm).
CNA's SpriteFont renders text left-to-right by default. For RTL support, use a
BiDi library (libfribidi or ICU) to reorder characters before passing to
DrawString, then draw with SpriteEffects::FlipHorizontally if needed.
Date/number formatting
// Simple locale-aware number formatter
std::string FormatNumber(int n, const std::string& lang) {
// Czech uses space as thousands separator
// English uses comma
// For simplicity: just convert to string
return std::to_string(n);
// Production: use std::locale or ICU
}
Detecting system locale
#include <cstdlib>
#include <string>
std::string DetectSystemLocale() {
#if defined(_WIN32)
char buf[16];
GetLocaleInfoA(LOCALE_USER_DEFAULT, LOCALE_SISO639LANGNAME, buf, 16);
return std::string(buf);
#else
const char* lang = std::getenv("LANG");
if (!lang) return "en";
std::string l(lang);
// e.g. "cs_CZ.UTF-8" -> "cs"
auto under = l.find('_');
if (under != std::string::npos) l = l.substr(0, under);
auto dot = l.find('.');
if (dot != std::string::npos) l = l.substr(0, dot);
return l.empty() ? "en" : l;
#endif
}
Hot-reload strings in debug
#ifdef _DEBUG
// In Update(): watch for F5 to reload strings without restarting
void Update(const GameTime& gt) override {
auto ks = Keyboard::GetState();
if (ks.IsKeyDown(Keys::F5) && !prevF5_) {
try {
localization_.Load(localization_.CurrentLang());
SDL_Log("Strings reloaded");
} catch (const std::exception& e) {
SDL_Log("Reload failed: %s", e.what());
}
}
prevF5_ = ks.IsKeyDown(Keys::F5);
}
#endif