Skip to content

icui18n

icui18n layers polymorphic resource bundle inheritance and locale-change events on top of ICU’s ResourceBundle API. It shares the same design philosophy as the Java libraries — bundles follow the class hierarchy, locale changes fire to listeners automatically, and every object manages its own locale independently — implemented in C++20.

Requires: C++20 · ICU4C · CMake 3.21+ · View on GitHub →


Add the following to your project’s CMakeLists.txt. ICU must be installed on the build machine; set ICU_ROOT if it is not on the default search path.

include(FetchContent)
FetchContent_Declare(
icui18n
GIT_REPOSITORY https://github.com/clydegerber/icui18n.git
GIT_TAG v1.0.0
)
FetchContent_MakeAvailable(icui18n)
target_link_libraries(my_target PRIVATE icui18n::icui18n)

The bundle chain owner and locale-event source. Holds the current icu::Locale, an ordered chain of ICU ResourceBundle nodes (most-derived class → hierarchy root), and a map of locale-change listeners. Key lookup walks the chain; ICU handles per-bundle locale fallback (en_US → en → root) automatically within each node. All public methods are thread-safe after construction.

CRTP mixin that wires a concrete class into the bundle chain. Declare two public static constexpr std::string_view members:

  • bundle_root — filesystem path to the directory containing the compiled .res files.
  • bundle_name — relative bundle path (e.g. com/example/ServiceBundle).

Two specialisations cover the two usage patterns:

SpecialisationWhen to use
LocalizableFor<Self>Self is the root of a localizable hierarchy
LocalizableFor<Self, Parent>Self extends a localizable parent class

Move-only RAII handle returned by addLocaleListener(). The listener is automatically deregistered when the subscription is destroyed or move-assigned over.

Non-owning (Localizable&, key) handle. Lazy accessors — getString(), getInteger(), getDouble(), getTable() — always reflect the source’s current locale with no caching.

Mixin for objects that display content from an external Localizable. Manages a Resource binding and an optional callback that fires when the source changes locale. Move-only; copy is deleted to prevent lambda capture bugs.


Bundles follow the class hierarchy. A subclass only needs the entries it overrides — everything else falls through to the parent bundle.

#include <icui18n/Localizable.hpp>
// Root of the hierarchy
class Service : public icui18n::LocalizableFor<Service>
{
public:
static constexpr std::string_view bundle_root = "/usr/share/myapp/i18n";
static constexpr std::string_view bundle_name = "com/example/ServiceBundle";
};
// Extends Service — inherits bundle_root, overrides "greeting"
class UserService : public icui18n::LocalizableFor<UserService, Service>
{
public:
static constexpr std::string_view bundle_name = "com/example/UserServiceBundle";
};

A cross-library subclass that must store its bundles under its own install path redeclares bundle_root independently:

class ExtService : public icui18n::LocalizableFor<ExtService, Service>
{
public:
static constexpr std::string_view bundle_root = "/usr/lib/ext/i18n";
static constexpr std::string_view bundle_name = "com/ext/ExtServiceBundle";
};

ICU resource bundles are plain-text .txt files compiled to binary .res files by genrb. The root bundle provides the fallback for all locales:

com/example/ServiceBundle/root.txt
root {
greeting { "Hello" }
farewell { "Goodbye" }
}
com/example/ServiceBundle/fr.txt
fr {
greeting { "Bonjour" }
farewell { "Au revoir" }
}

Compile with:

Terminal window
genrb -d /usr/share/myapp/i18n/com/example/ServiceBundle root.txt fr.txt

Service svc;
svc.setBundleLocale(icu::Locale::getFrench());
std::optional<icu::UnicodeString> greeting = svc.getString("greeting"); // "Bonjour"
std::optional<std::int32_t> retries = svc.getInteger("max_retries");
std::optional<double> ratio = svc.getDouble("threshold"); // stored as string
std::optional<icu::ResourceBundle> errors = svc.getTable("errors");

auto sub = svc.addLocaleListener(
[](const icu::Locale& prev, const icu::Locale& next)
{
// re-render, reformat, etc.
});
svc.setBundleLocale(icu::Locale::getGerman()); // listener fires here
// sub goes out of scope → listener automatically deregistered

Objects that display content from an external Localizable without owning a bundle:

#include <icui18n/Resourceful.hpp>
class GreetingLabel : public icui18n::Resourceful
{
public:
void bind(const Service& svc)
{
setResource({svc, "greeting"},
[this](const icu::Locale&, const icu::Locale&)
{
refresh();
});
}
void refresh()
{
if (auto r = getResource())
display(r->getString());
}
};

  • getString, getInteger, getDouble, getTable, getBundleLocale, addLocaleListener — shared lock; safe to call concurrently with each other.
  • setBundleLocale — exclusive lock during bundle installation; listeners are fired outside the lock. Not safe to call concurrently with itself.