Tutorial 98: Localization and Multiple Languages

CNA — C++ XNA 4.0 reimplementation

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