MeshLib Documentation
Loading...
Searching...
No Matches
Internationalization Guide

MeshLib uses a gettext-compatible localization system backed by Boost.Locale. Translatable strings are marked in C++ source code, extracted to template files, translated per language, compiled to binary catalogs, and loaded at runtime.

This guide explains how to integrate and use the i18n system in your own MeshLib-based project.

File formats and directory layout

The gettext toolchain uses three file types:

  • .pot - Master list of all extractable strings for a domain. Generated by xgettext.
  • .po - Per-language translation file. Each msgid has a corresponding msgstr filled in by a translator.
  • .mo - Compiled from .po files by msgfmt. Loaded at runtime by Boost.Locale.

Source translations live under a locale/ directory at the project root:

locale/
MyPlugin.pot <- template (all extractable strings)
MRRibbonMyMenu.pot <- template for ribbon JSON strings
de/
MyPlugin.po <- German translations
MRRibbonMyMenu.po
fr/
MyPlugin.po <- French translations
MRRibbonMyMenu.po

At build time, .po files are compiled to .mo and placed in the output directory:

<build>/locale/<lang>/LC_MESSAGES/<Domain>.mo

For example: build/Release/bin/locale/de/LC_MESSAGES/MyPlugin.mo.

Note
The domain name is always derived from the filename stem: MyPlugin.po belongs to domain "MyPlugin".

Gettext utilities and helper scripts

Prerequisites

  • gettext tools: xgettext, msgmerge, msgfmt. Install via your OS package manager or from vcpkg (vcpkg install gettext[tools]). Alternatively, set the GETTEXT_ROOT environment variable to a standalone gettext install path (e.g. gettext-tools-windows).
  • Python 3 for the helper scripts in scripts/gettext/.

Helper scripts

All scripts are in scripts/gettext/:

  • update_translations.py - Runs xgettext over C++ sources to produce or update a .pot template, then runs msgmerge to bring existing .po files in sync.
  • update_json_translations.py - Extracts translatable fields from .items.json and .ui.json ribbon menu files into a .pot, then merges existing .po files.
  • compile_translations.py - Compiles every .po file under locale/ to .mo using msgfmt.

Extracting C++ strings

python3 scripts/gettext/update_translations.py \
locale/MyPlugin.pot \
source/MyPlugin/

This scans all .cpp, .h, and .hpp files and extracts strings marked by the recognized macros.

Extracting JSON strings

python3 scripts/gettext/update_json_translations.py \
locale/MRRibbonMyMenu.pot \
source/MyPlugin/MRRibbonMyMenu.items.json

Compiling translations

python3 scripts/gettext/compile_translations.py \
locale/ \
build/Release/bin/locale/

This produces build/Release/bin/locale/<lang>/LC_MESSAGES/<Domain>.mo for every .po found.

CMake integration

cmake/Modules/I18nHelpers.cmake provides the mr_add_translations() function:

include(I18nHelpers)
mr_add_translations(myplugin_translations
DOMAINS
MyPlugin
MRRibbonMyMenu
PATHS
"${CMAKE_CURRENT_PROJECT_DIR}/locale"
)
if(TARGET myplugin_translations)
add_dependencies(MyApp myplugin_translations)
endif()

The function:

  1. Finds msgfmt (respects GETTEXT_ROOT / $ENV{GETTEXT_ROOT}).
  2. For each domain and path, locates every <path>/<lang>/<domain>.po file.
  3. Creates custom commands that compile each .po to .mo at build time.
  4. Creates a custom target depending on all .mo files.
  5. Installs .pot templates and compiled .mo files to ${MR_RESOURCES_DIR}/locale/.

If msgfmt is not found, the function silently does nothing and no target is created. This is why the if(TARGET ...) guard is needed before add_dependencies.

MSBuild integration

source/CompileTranslations.targets provides an equivalent build-time .po.mo compilation step for Visual Studio projects. Import the targets file and declare one or more TranslationLocaleDir items pointing at your locale/ root directories:

<Project>
<!-- Tell CompileTranslations where your .po files live. -->
<ItemGroup>
<TranslationLocaleDir Include="$(SolutionDir)..\locale" />
</ItemGroup>
<Import Project="$(MeshLibDir)\source\CompileTranslations.targets" />
</Project>

MeshLib's own common.props already adds $(MeshLibDir)\locale to TranslationLocaleDir, so projects that import common.props inherit MeshLib's translations automatically.

The CompileTranslations target runs before Build and:

  1. Globs every *.po file in specified locale dirs.
  2. Creates $(OutDir)locale\<lang>\LC_MESSAGES\ output directories.
  3. Calls msgfmt --check on each .po to produce the corresponding .mo.

If msgfmt is not found, the target is silently skipped and the build continues without compiling translations.

In-app initialization and configuration

Automatic initialization

Some configuration is performed automatically during the MRViewer library loading:

  1. Registration of the default domain "MeshLib".
  2. Adding the default catalog path: SystemPath::getResourcesDirectory() / "locale".

The initial locale is "en" (i.e. no translations applied).

Registering a custom domain

Call addDomain() once and use a returned value from findDomain(), before any translations from that domain are needed:

#include <MRViewer/MRLocale.h>
MR::Locale::addDomain( "MyPlugin" );
static const int kMyDomainId = MR::Locale::findDomain( "MyPlugin" );

The returned integer is stable for the process lifetime. You can pass it to translate() via MR::Locale::Domain{ kMyDomainId }.

Registering a custom catalog path

If your plugin stores .mo files outside the standard resources directory, register the path before any UI is shown:

MR::Locale::addCatalogPath( myPlugin.resourceDir() / "locale" );

Paths are deduplicated. Every call to addCatalogPath or addDomain regenerates the active locale object, so new catalogs are available immediately.

Switching locale at runtime

MR::Locale::set( "ko" ); // switch to Korean
MR::Locale::set( "en" ); // back to English

set() fires the onChanged signal synchronously before returning.

Reacting to locale changes

boost::signals2::connection conn = MR::Locale::onChanged( [&]( const std::string& localeName )
{
// rebuild cached translated strings, tooltips, etc.
rebuildUI();
} );

Defining human-readable locale names

Many common languages and regions already have a pre-loaded human-readable name provided by CLDR. If your locale is not in the list, you can add it in runtime.

MR::Locale::setDisplayName( "my_variant", "My Language (Special)" );

Translation functions and macros

Macros

All macros are defined in MRViewer/MRI18n.h, except _t, which is in MRMesh/MRMeshFwd.h:

  • _tr("text") - UI labels passed directly to ImGui/UI functions.
  • s_tr("text") - When you need an owning string (storage, concatenation).
  • f_tr("text {}") - Format strings for fmt::format().
  • _t("text") - Marking strings for extraction in contexts where the original value must be returned (see an example below).
Warning
_tr() returns a pointer into a temporary std::string that is destroyed at the end of the full expression. Always consume _tr() in the same expression, or use s_tr() / MR::Locale::translate() to store the result.
// CORRECT: consumed in the same expression
UI::button( _tr( "Save" ), size );
ImGui::Text( "%s", _tr( "Label" ) );
// WRONG: dangling pointer
const auto label = _tr( "Save" );
UI::button( label, size ); // undefined behavior
// CORRECT: store as std::string
const auto label = s_tr( "Save" );
UI::button( label.c_str(), size );

Direct translate functions

MR::Locale::translate() accepts an optional Domain parameter:

// simple message (default MeshLib domain)
std::string t = MR::Locale::translate( "Open" );
// with explicit domain
std::string t = MR::Locale::translate( "Open", MR::Locale::Domain{ kMyDomainId } );
// context disambiguation
std::string t = MR::Locale::translate( "Camera", "View" );
// plural form
std::string t = MR::Locale::translate( "%d item", "%d items", count );
// batch translation for combo lists
auto items = MR::Locale::translateAll( kModeNames );
auto items = MR::Locale::translateAll( "context", kModeNames );

Static arrays and deferred translation

Mark strings with _t() at declaration time so xgettext extracts them, then translate at display time:

// declaration: _t() is a no-op, strings are stored untranslated
static const std::vector<std::string> kModeNames {
_t( "Append" ),
_t( "Replace" ),
};
// display: translate on every frame
UI::combo( _tr( "Mode" ), &idx, MR::Locale::translateAll( kModeNames ) );

Context disambiguation

When the same English string has different meanings, use the context overload:

// "View" as a noun (saved camera view) vs. verb (to view something)
ImGui::Text( "%s", _tr( "Camera", "View" ) );
ImGui::Text( "%s", _tr( "Action", "View" ) );

In the .po file these appear as separate entries with different msgctxt values.

Plural forms

auto label = MR::Locale::translate( "%d item", "%d items", count );

The correct form is selected by Boost.Locale based on the Plural-Forms: rule in the .po header.

TRANSLATORS comments

Place a // TRANSLATORS: comment on the line immediately before a translatable string to give translators context. xgettext picks these up and includes them as #. comment lines in the .pot file:

// TRANSLATORS: Shown when the mesh has no faces after repair
ImGui::Text( "%s", _tr( "Empty result" ) );

Custom header file for your domain

When building a plugin with its own translation domain, create a project-local header file that redefines the macros to use your domain automatically. This way, every _tr() call in your plugin looks up the correct catalog without passing domain IDs explicitly.

Copy and adapt the following template (also available at examples/cpp-samples/MRI18nDomainExample.h):

#pragma once
#include <MRViewer/MRI18n.h>
// forward declaration from MRLocale.h
namespace MR::Locale { MRVIEWER_API int findDomain( const char* domainName ); }
// replace "MyPlugin" with your actual domain name
// (must match the .pot/.po filename stem)
inline constexpr const char* MY_PLUGIN_I18N_DOMAIN = "MyPlugin";
// redefine the i18n macros to use your domain by default
#undef _tr
#undef s_tr
#undef f_tr
#define _tr( ... ) MR::Locale::translate( __VA_ARGS__, MR::Locale::Domain{ MR::Locale::findDomain( MY_PLUGIN_I18N_DOMAIN ) } ).c_str()
#define s_tr( ... ) MR::Locale::translate( __VA_ARGS__, MR::Locale::Domain{ MR::Locale::findDomain( MY_PLUGIN_I18N_DOMAIN ) } )
#define f_tr( ... ) fmt::runtime( MR::Locale::translate( __VA_ARGS__, MR::Locale::Domain{ MR::Locale::findDomain( MY_PLUGIN_I18N_DOMAIN ) } ) )

To use this header in your plugin:

  1. Include your local header file instead of <MRViewer/MRI18n.h> in every .cpp file.
  2. Register the domain at startup: MR::Locale::addDomain( MY_PLUGIN_I18N_DOMAIN );
  3. All _tr(), s_tr(), and f_tr() calls will now use your domain.
Note
findDomain() performs a cache lookup by pointer address for const char* literals, so the runtime cost is minimal.

Translating JSON menu files

MeshLib's ribbon menu system loads tool definitions from .items.json and UI layout from .ui.json files. Both formats contain translatable strings.

.items.json

Each entry in the "Items" array may contain:

  • "Caption" - the display label (falls back to "Name" if absent).
  • "Tooltip" - the tooltip text.
{
"Items": [
{
"Name": "My Tool",
"Caption": "My Custom Tool",
"Tooltip": "Performs a custom operation on the mesh"
}
]
}

See C++ Example Plugin Overview for a complete plugin JSON example.

.ui.json

Tab names in the "Tabs" array are extracted with the context "Tab name":

{
"Order": 10,
"LibName": "MyPlugin",
"Tabs": [
{
"Name": "Analysis",
"Groups": [ ... ]
}
]
}

Domain auto-registration

When the ribbon schema loader reads a .items.json file, it automatically registers a domain from the filename stem. For example, loading MRRibbonMyMenu.items.json calls Locale::addDomain( "MRRibbonMyMenu" ) and stores the returned domain ID in each MenuItemInfo. The matching .ui.json then uses Locale::findDomain() with the same stem.

This means ribbon menu strings are translated from their own domain catalog, separate from your C++ code strings.

Extracting JSON strings

Run update_json_translations.py for each JSON pair:

python3 scripts/gettext/update_json_translations.py \
locale/MRRibbonMyMenu.pot \
source/MyPlugin/MRRibbonMyMenu.items.json

The script extracts "Caption" (or "Name") and "Tooltip" from .items.json, and tab "Name" fields (with context "Tab name") from the matching .ui.json.