Skip to content

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

3 Commits

Folders and files

Repository files navigation

Drogon-I18n

High-Performance, Zero-Lock Internationalization (i18n) & Localization (l10n) Plugin for the Drogon C++ Web Framework

C++ Standard Framework License Platforms

drogon_i18n is a modern, thread-safe, and lightning-fast internationalization plugin designed specifically for the Drogon C++ web framework. Built from the ground up for high-concurrency systems, it leverages a cold-start memory preloading pattern to deliver Zero-Lock $O(1)$ runtime dictionary lookups across all worker threads.


Key Features

  • Zero-Lock Runtime Execution: Dictionaries are loaded once during application bootstrap (initAndStart). Once running, memory data structures become immutable, enabling lock-free, concurrent $O(1)$ read operations across all event-loop worker threads.
  • Modern C++20 Architecture: Fully compliant with C++20 standards, utilizing modern filesystem features, UTF-8 path conversions (generic_u8string), and string views.
  • Flexible Dictionary Providers:
    • JSON Loader (JsonProvider): Supports both flat key-value pairs and deeply nested JSON objects with automatic Dot-Notation Key Flattening (e.g., {"auth": {"login": {"title": "Welcome"}}} $\rightarrow$ "auth.login.title").
    • GNU Gettext Loader (MoProvider): Direct binary parsing of compiled GNU Gettext .mo files with automatic endianness detection/swapping and Zero-Heap buffer allocations.
    • Extensible Provider Architecture: Implement the I18nProvider interface to add support for YAML, XML, or database-driven dictionary sources.
  • Smart Dynamic Locale Resolution: Automatically detects client language preference from incoming HttpRequestPtr using a customizable multi-tier fallback pipeline:
    1. Request Attributes (e.g., injected by custom filters/middlewares)
    2. HTTP Session Data (req->getSession())
    3. URL Query Parameters (e.g., ?lang=ar)
    4. HTTP Accept-Language Header (with short-code fallback matching like ar-EG $\rightarrow$ ar)
    5. Configured Default / Fallback Locale
  • Thread-Safe Localized Formatting: Manages pre-constructed system std::locale instances per registered language. Enables localized formatting for numbers, currencies, and dates without modifying global process locale state (std::setlocale).
  • Seamless CSP (Drogon Server Pages) Integration: Provides concise, zero-cost helper macros (tr, trl, vtr, vtrd, vtrl) and the ViewHelper response builder to easily translate text directly within C++ .csp views.
  • Deterministic Namespace Hierarchy: Subdirectories and file names automatically map into explicit namespaces (e.g., locales/ar-eg/erp/accounting.json $\rightarrow$ ns = "erp::accounting").
  • Enterprise Security & Reliability:
    • Path Traversal Protection: Guarded directory scanning using canonical path verification.
    • Stack Overflow Recursion Guards: Prevents JSON depth-based Denial of Service (DoS) attacks (MAX_JSON_DEPTH = 32).
    • Buffer Overflow Protection: Strict boundary checks for binary Gettext offsets.
  • Cross-Platform Compatibility: Fully tested on Linux (GCC / Clang), macOS (Apple Clang), and Windows (MSVC).

Directory Structure & Conventions

drogon_i18n expects a structured locale directory (configurable via locales_directory). Each locale folder contains a metadata file named _info_.json alongside translation files.

locales/
├── ar-eg/
│   ├── _info_.json
│   ├── main.json
│   ├── gettext.mo
│   └── erp/
│       └── accounting.json
└── en-us/
    ├── _info_.json
    └── main.json

Locale Metadata (_info_.json)

Every language folder must include an _info_.json metadata file:

{
  "status": true,
  "key": "ar-eg",
  "longCode": "ar_EG",
  "shortCode": "ar",
  "rtl": true,
  "title": "العربية",
  "mainDictionary": "main"
}
Field Type Description
status bool Enables or disables the locale. Disabled locales are skipped during boot.
key string Unique identifier key used by the application (e.g., "ar-eg", "en-us").
longCode string POSIX/OS language code used for std::locale construction (e.g., "ar_EG").
shortCode string ISO 639-1 two-letter code for HTTP Accept-Language fallback matching.
rtl bool Layout direction flag (Right-To-Left).
title string Display name of the language (e.g., "العربية").
mainDictionary string Default dictionary file stem used when no explicit namespace is specified.

Automatic Namespace Resolution Rules

  • locales/en-us/main.json $\rightarrow$ Namespace: "main" or ""
  • locales/en-us/gettext.mo $\rightarrow$ Namespace: "gettext"
  • locales/en-us/erp/accounting.json $\rightarrow$ Namespace: "erp::accounting"

Installation & Build Instructions

Dependencies

  • C++ Compiler: C++20 compliant (GCC 11+, Clang 13+, MSVC 2019+)
  • CMake: Version 3.15 or higher
  • Drogon Framework: v1.9.0 or higher

Building as a Static or Shared Library

git clone https://github.com/STCodePro/Drogon-I18n.git
cd Drogon-I18n

mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_EXAMPLES=ON
make -j$(nproc)
sudo make install

Integrating with CMake Projects

Option A: Using find_package (Installed System-Wide)

find_package(Drogon CONFIG REQUIRED)
find_package(drogon_i18n REQUIRED)

add_executable(my_drogon_app main.cpp)
target_link_libraries(my_drogon_app PRIVATE drogon_i18n Drogon::Drogon)

Option B: Using add_subdirectory (Embedded Source)

add_subdirectory(third_party/Drogon-I18n)

add_executable(my_drogon_app main.cpp)
target_link_libraries(my_drogon_app PRIVATE drogon_i18n)

Configuration

drogon_i18n can be initialized programmatically or via Drogon’s config.json configuration file.

Programmatic Setup (main.cpp)

#include <drogon/drogon.h>
#include <drogon/i18n/I18nPlugin.h>

int main()
{
    Json::Value pluginConfig;
    pluginConfig["locales_directory"] = "./locales";
    pluginConfig["default_locale"] = "en-us";
    pluginConfig["fallback_locale"] = "en-us";
    
    // Configurable Key Resolutions
    pluginConfig["locale_attribute_key"] = "lang";
    pluginConfig["locale_session_key"]   = "lang";
    pluginConfig["locale_parameter_key"] = "lang";
    pluginConfig["locale_view_data_key"] = "lang";
    pluginConfig["request_view_key"]     = "req";
    pluginConfig["default_main_dictionary"] = "main";

    drogon::app().addPlugin("drogon::i18n::I18nPlugin", {}, pluginConfig);

    drogon::app().addListener("127.0.0.1", 8080).run();
    return 0;
}

Drogon config.json Setup

{
  "plugins": [
    {
      "name": "drogon::i18n::I18nPlugin",
      "dependencies": [],
      "config": {
        "locales_directory": "./locales",
        "default_locale": "en-us",
        "fallback_locale": "en-us",
        "locale_attribute_key": "lang",
        "locale_session_key": "lang",
        "locale_parameter_key": "lang",
        "locale_view_data_key": "lang",
        "request_view_key": "req",
        "default_main_dictionary": "main"
      }
    }
  ]
}

Usage Examples

1. Basic Translation in Handlers / Controllers

You can translate messages using the Translator class or the shorthand tr and trl macros.

#include <drogon/drogon.h>
#include <drogon/i18n/Translator.h>

using namespace drogon;
using namespace drogon::i18n;

void handleWelcome(const HttpRequestPtr &req,
                   std::function<void(const HttpResponsePtr &)> &&callback)
{
    std::string name = req->getParameter("name");
    if (name.empty()) name = "Guest";

    // 1. Shorthand macro with automatic HTTP Request locale resolution & template variable interpolation
    std::string welcomeMsg = tr(req, "welcome", {{"name", name}});

    // 2. Explicit namespace translation ("erp::accounting")
    std::string invoiceMsg = tr(req, "invoice_created", "erp::accounting", {{"id", "1001"}});

    // 3. Explicit locale translation ignoring request context
    std::string arMsg = trl("ar-eg", "goodbye");

    auto resp = HttpResponse::newHttpResponse();
    resp->setBody(welcomeMsg + "\n" + invoiceMsg + "\n" + arMsg);
    callback(resp);
}

2. Thread-Safe Localized Formatting (std::locale)

LocaleManager handles pre-constructed std::locale instances without modifying the global process state (std::setlocale), guaranteeing thread-safety.

#include <drogon/i18n/LocaleManager.h>
#include <drogon/i18n/Translator.h>
#include <sstream>

void handleFormat(const HttpRequestPtr &req,
                  std::function<void(const HttpResponsePtr &)> &&callback)
{
    std::string activeLang = Translator::getLocaleFromRequest(req);

    // Retrieve pre-constructed system std::locale for active language
    const std::locale &sysLoc = LocaleManager::instance().getSystemLocale(activeLang);

    std::ostringstream ss;
    ss.imbue(sysLoc); // Apply locale safely to stream
    
    ss << "Formatted Number: " << std::fixed << 1234567.89;

    auto resp = HttpResponse::newHttpResponse();
    resp->setBody(ss.str());
    callback(resp);
}

3. Rendering Drogon CSP Views (ProfileView.csp)

drogon_i18n provides ViewHelper and dedicated CSP helper macros (vtr, vtrd, vtrl) to render localized templates cleanly.

Controller Code:

#include <drogon/i18n/ViewHelper.h>

void handleProfile(const HttpRequestPtr &req,
                    std::function<void(const HttpResponsePtr &)> &&callback)
{
    HttpViewData data;
    data.insertAsString("name", "Ahmed");

    // Automatically injects resolved locale and req pointer into HttpViewData
    auto resp = ViewHelper::newI18nHttpViewResponse(req, "ProfileView", data);
    callback(resp);
}

CSP Template (ProfileView.csp):

<%inc
#include <drogon/i18n/ViewHelper.h>
%>
<%c++
auto req = @@.get<drogon::HttpRequestPtr>("req");
auto name = @@.get<std::string>("name");
%>
<!DOCTYPE html>
<html>
<head>
    <title>{% vtrd(@@, "profile"); %}</title>
</head>
<body>
    <h1>{% vtr(req, "welcome", {{"name", name}}); %}</h1>
    <p>{% vtr(req, "invoice_created", "erp::accounting", {{"id", "505"}}); %}</p>
</body>
</html>

4. Working with GNU Gettext Binary .mo Files

  1. Create a Gettext source file (locales/ar-eg/gettext.po):
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"

msgid "hello_gettext"
msgstr "مرحباً بك من ملف Gettext الثنائي!"
  1. Compile the .po file into a binary .mo file using msgfmt:
msgfmt -o locales/ar-eg/gettext.mo locales/ar-eg/gettext.po
  1. Call the translated string in your code (mapped to namespace "gettext"):
std::string msg = tr(req, "hello_gettext", "gettext");

5. Writing a Custom I18nProvider (e.g., Database or YAML)

You can extend drogon_i18n by implementing the I18nProvider interface and registering it with the plugin.

#include <drogon/i18n/I18nProvider.h>
#include <drogon/i18n/I18nPlugin.h>
#include <drogon/i18n/Translator.h>

class CustomYamlProvider : public drogon::i18n::I18nProvider
{
public:
    bool load(const std::string &filePath,
              const std::string &langKey,
              const std::string &ns) override
    {
        // Parse custom file format...
        std::string key = "custom_key";
        std::string value = "Custom Value";

        // Store into global dictionary
        drogon::i18n::Translator::addTranslation(langKey, ns, key, value);
        return true;
    }
};

// Register in main.cpp or startup code:
auto plugin = drogon::app().getPlugin<drogon::i18n::I18nPlugin>();
if (plugin)
{
    plugin->registerProvider(".yaml", std::make_shared<CustomYamlProvider>());
}

Macro Quick Reference

Macro Signature Context / Usage
tr(req, key, ...) translate(HttpRequestPtr, key, ns, args) Main translation shorthand using HTTP Request
trl(locale, key, ...) localeTranslate(locale, key, ns, args) Translation shorthand using explicit locale string
vtr(req, key, ...) Translator::translate(req, key, ns, args) View shorthand for HTTP Request context
vtrd(data, key, ...) ViewHelper::translate(HttpViewData, key, ns, args) View shorthand for HttpViewData (@@) context
vtrl(locale, key, ...) Translator::localeTranslate(locale, key, ns, args) View shorthand for explicit locale string

Architecture & Performance Benchmarks

Cold-Start Warm-up & Immutable Memory Model

Traditional i18n libraries suffer from runtime lock contention or file I/O latency. drogon_i18n solves this using an immutable memory model:

[ Server Initialization Phase (Single-Threaded) ]
1. I18nPlugin::initAndStart() runs.
2. Scans locales directory and parses _info_.json metadata.
3. Providers (JsonProvider, MoProvider) parse files.
4. Pre-constructs std::locale objects in LocaleManager.
5. Populates global dictionary map: "langKey::ns::key" -> "value".

[ Server Runtime Phase (Multi-Threaded Event Loops) ]
1. Thousands of incoming HTTP requests arrive simultaneously across worker threads.
2. Translator performs direct O(1) lock-free std::unordered_map lookup.
3. Zero lock acquisition, zero atomic counter bouncing, zero file I/O.

Contributing

Contributions, bug reports, and feature requests are welcome!

  1. Fork the repository on GitHub.
  2. Create a feature branch (git checkout -b feature/AmazingFeature).
  3. Commit your changes (git commit -m 'Add some AmazingFeature').
  4. Push to the branch (git push origin feature/AmazingFeature).
  5. Open a Pull Request.

Author & License

  • Author: Ahmed Sabri (STCodePro)
  • License: MIT License. See LICENSE file for details.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages