Internationalizing Plug-Ins
[Theory] Introduction
Internationalizing a plug-in means making its user interface appear in the native language of the user. This is also known as “localizing”, or “i18n”. Internally GIMP uses the i18n system called “gettext”, and so do plug-ins.
Because C and Introspected (e.g. Python) plug-ins are both served by the very
same libgimp infrastructure, this tutorial therefore covers both languages at once, and only the language-specific bits are specificed in the tutorial,
along with Script-Fu intrinsics.
The process is:
- Mark the user-facing strings in your source
- Use the i18n function if needed
- Coordinate with translators to create one or
more translation data files (
.po, “portable” object files) - Build and install the compiled
translation data (
.mo, “machine” object files)
More details and examples follow.
[Code] Marking Strings in a Plug-in
The gettext API is quite straight-forward.
_()is a shortcut forgettext(): it translates a single string;N_()does nothing at runtime, it only tells the extraction tools that the string it wraps must be translated.
You need N_() for strings which are initialized as compile-time
constants, typically in a static table, where _() would not even
compile.
Use _() macro in the source of a plugin to
mark user-facing GUI strings that should be internationalized.
Typically, you mark these strings:
- The label of the menu item of the plugin
- Labels on widgets in a dialog for the plugin
- Error and information messages to the user
_() macro which marks an internationalizable string with
the _ which designates a keyboard shortcut.C Plug-in marking
Now let’s take the create_procedure() function we wrote in the
basic C tutorial and mark the strings which a
user will actually read:
static GimpProcedure *
hello_world_create_procedure (GimpPlugIn *plug_in,
const gchar *name)
{
GimpProcedure *procedure = NULL;
if (g_strcmp0 (name, PLUG_IN_PROC) == 0)
{
procedure = gimp_image_procedure_new (plug_in, name,
GIMP_PDB_PROC_TYPE_PLUGIN,
hello_world_run, NULL, NULL);
gimp_procedure_set_sensitivity_mask (procedure, GIMP_PROCEDURE_SENSITIVE_ALWAYS);
gimp_procedure_set_menu_label (procedure, _("_C Hello World"));
gimp_procedure_add_menu_path (procedure, "<Image>/Hell_o Worlds/");
gimp_procedure_set_documentation (procedure,
_("Official Hello World Tutorial in C"),
_("Some longer text to explain about this procedure. "
"This is mostly for other developers calling this procedure."),
NULL);
gimp_procedure_add_double_argument (procedure, "opacity",
_("O_pacity"), _("Opacity"),
0.0, 100.0, 100.0,
GIMP_PARAM_READWRITE);
gimp_procedure_add_int_argument (procedure, "size",
_("Si_ze"), _("Size in pixels"),
1, 1000, 20,
GIMP_PARAM_READWRITE);
gimp_procedure_set_attribution (procedure, "Jehan",
"Jehan, ZeMarmot project",
"2025");
}
return procedure;
}I added 2 arguments to this demo plug-in (opacity and size)
so that we have some labels to mark; also, the displayed
procedure name and the help tooltip were translated.
And in run(), the error messages which end up in front of the user
are marked too:
g_set_error (&error, GIMP_PLUG_IN_ERROR, 0,
_("Procedure '%s' works with zero or one layer."),
PLUG_IN_PROC);Python Plug-In marking
The Python API is the same C API, so the same strings are marked in the same places. The only difference is that Python has no macros, so we write our 2 helpers ourselves:
import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
from gi.repository import GLib
# Pass None instead of 'c-hello-world' to use the default GIMP domain for this plug-in.
def N_(message): return message
def _(message): return GLib.dgettext('c-hello-world', message)Usage is identical to the C version. And this is not a GIMP-specific convention, this is how the whole Python ecosystem does gettext.
Script-Fu Plug-In marking
In Scheme, the _ marker character must precede a
double quote character. Example:
SF-OPTION _"Orientation" '(_"Horizontal" _"Vertical")N_ for Script-Fu.[Theory] The Default Localization Scheme
GIMP already does the hard part for you. Before it creates or runs any of your procedures, the plug-in infrastructure picks a gettext domain and a catalog directory, then calls appropriate functions.
By default:
- the domain is the name of your plug-in’s own directory (not the PDB procedure);
- the catalog directory is the
localesubdirectory of that very same directory.
Therefore, if you organize your plug-in out like this:
.../plug-ins/c-hello-world/
├── c-hello-world
└── locale/
├── de/LC_MESSAGES/c-hello-world.mo
└── fr/LC_MESSAGES/c-hello-world.mothen your domain is c-hello-world, and GIMP will look for
locale/de/LC_MESSAGES/c-hello-world.mo when the user speaks German,
and the fr equivalent when they speak French.
In other words, if want to localize your plug-in and respect this layout, you don’t have to write a single line of code to declare your domain. For Script-Fu, you should declare the domain as it will be instructed below.
[Code] Customizing or Disabling Localization
Sometimes the default behavior
is not what you want.
This is what the
set_i18n()
virtual method is for, with some differences on Python and Script-Fu.
What is common among these languages is that it is called by
the infrastructure before it creates or runs any of your procedures.
C Plug-In declaration
In C, you implement it as any other plug-in virtual method, i.e. add
it to your class and set it in class_init():
static gboolean
hello_world_set_i18n (GimpPlugIn *plug_in,
const gchar *procedure_name,
gchar **gettext_domain,
gchar **catalog_dir)
{
*gettext_domain = g_strdup ("zemarmot-c-demo");
*catalog_dir = g_strdup ("locale"); /* relative to the plug-in dir, you should rarely need it */
return TRUE;
}
static void
hello_world_class_init (HelloWorldClass *klass)
{
GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS (klass);
plug_in_class->query_procedures = hello_world_query_procedures;
plug_in_class->create_procedure = hello_world_create_procedure;
plug_in_class->set_i18n = hello_world_set_i18n;
}Sometimes you simply have no translations at all.
Since GIMP expects a locale directory by default, it will print a
warning on stderr every time it creates one of your procedures:
[plug-in-zemarmot-c-demo-hello-world] The catalog directory does not exist: ...
[plug-in-zemarmot-c-demo-hello-world] Override method set_i18n() for the plug-in to customize or disable localization.
[plug-in-zemarmot-c-demo-hello-world] Localization disabledYou can implement the method and simply refuse localization, and the warning goes away:
static gboolean
hello_world_set_i18n (GimpPlugIn *plug_in,
const gchar *procedure_name,
gchar **gettext_domain,
gchar **catalog_dir)
{
return FALSE;
}Python Plug-In declaration
And the Python equivalent of the above is a do_set_i18n() method
returning False:
def do_set_i18n(self, procedure_name):
return False, None, NoneThose 2 Nones are the gettext_domain and catalog_dir
output arguments of the method, which Python expects you to return as
a tuple along with the return value.
Script-Fu Plug-In declaration
On Script-Fu, script-fu-register-i18n must appear at the
outermost lexical level of a script:
(script-fu-register-i18n "PDB name"
"domainName" | "Standard" | "None"
["relative path"]) => voidThat function takes two or three arguments and returns nothing:
- The first argument is of type string and is the name of a PDB procedure;
- The second argument is of type string and is either a domain name in the i18n system (the name of the translation file) OR “Standard” (file name where the PDB procedure is defined) OR, rarely, “None”;
- The third (optional) argument is of type string and is a relative path to a “catalog” directory. The path must be below the parent directory of the file where the PDB procedure is defined. Anyway, you should rarely need it. When the third argument is not provided, the i18n system uses as “catalog” a standard directory named “locale”.
[Theory] The Role of Translators
This briefly explains how translators work together with plug-in authors, which is basically all standard gettext workflow, the same one GIMP itself uses.
A translator is usually fluent in English and one target language. You may need many translators for many target languages.
How translation files are created can differ between plug-ins.
The .po files are usually kept as “generated” source in the source
code repository, or at least next to the plug-in sources.
The “p” means “portable.”
A person in the role of translator can use software tools to scan the
source to produce .po files.
It is also possible that the plug-in author provides updated .po
files or template .pot files for translation. For example:
# Extract the marked strings into a template file.
xgettext --from-code=UTF-8 --add-comments \
--keyword=_ --keyword=N_ \
-o c-hello-world.pot c-hello-world.c-L Python to after xgettext.Then the translator uses software tools to edit .po files, changing
the translated second half of the translation string pairs.
(The second half was initially untranslated.)
When you change user-facing strings in the source code, translators can
update the .po files with msgmerge, without starting the whole
process over again. For example:
# Merge the template into each translator's file.
msgmerge --update de.po c-hello-world.pot
msgmerge --update fr.po c-hello-world.potA typical source tree therefore looks like this:
c-hello-world/
├── c-hello-world.c the plug-in source
├── c-hello-world.pot the template, extracted from the source
├── LINGUAS a text file listing "de" and "fr"
├── de.po (from translators)
├── fr.po (from translators)
└── meson.build (optional, see below)[Code] Building and Installing the Translations
At build time, another tool compiles the .po files into binary .mo
files.
The “m” means machine.
The .mo files are compact and the ones installed with a plug-in,
and the only ones GIMP ever reads at runtime.
# Compile the translations, right where GIMP will look for them.
mkdir -p locale/de/LC_MESSAGES locale/fr/LC_MESSAGES
msgfmt --check -o locale/de/LC_MESSAGES/c-hello-world.mo de.po
msgfmt --check -o locale/fr/LC_MESSAGES/c-hello-world.mo fr.poIf your plug-in is already built and installed, for example, with
meson, the whole workflow above is 3 lines
of build definition, in a meson.build file next to your .po files:
i18n = import('i18n')
i18n.gettext('c-hello-world',
preset: 'glib',
install_dir: get_option('prefix') / get_option('libdir') /
'plug-ins' / 'c-hello-world' / 'locale')- the first argument is the gettext domain, i.e. the name of your plug-in’s directory;
preset: 'glib'is what tellsxgettextabout the_()andN_()markers, and about which of our functions take format strings (g_set_error()and friends);install_diris thelocaledirectory of your plug-in. It is not so relevant, since you will ideally distribute your plug-in to be installed in the user config dir so it work on all GIMP packages.
If you distribute your plug-in as a package, the .po is one more file to
install or copy; see our
plug-in packaging guides for the
platform-specific directories.
[Theory] Suites of Plug-Ins
Say you ship a suite of related plug-ins which share the same set of
phrases.
Since translators would otherwise have to translate the same strings
over and over, it is nicer for everyone to use a single domain, and
therefore a single set of .po files.
The catch is that the catalog directory is always relative to each
plug-in’s own directory, and set_i18n() functions may
not point outside of it.
So the very same .mo files have to be installed in the locale
directory of every plug-in of the suite:
.../plug-ins/
├── mySuite/
│ ├── mySuite the plug-in executable
│ └── locale/
│ ├── es/LC_MESSAGES/mySuite.mo
│ └── fr/LC_MESSAGES/mySuite.mo
└── mySuiteMore/
├── mySuiteMore another plug-in of the suite
└── locale/
├── es/LC_MESSAGES/mySuite.mo
└── fr/LC_MESSAGES/mySuite.moDuplicating a compiled file is not very elegant, but it is what GIMP itself does in such cases, and it is only a build-time detail your users never see.
Conclusion
As you see, internationalizing a plug-in is a quick 4-step affair.
- Back to “How to write a plug-in” tutorial index
- Previous tutorial: Calling GEGL Operations
- Next tutorial: Writing a GEGL filter