Skip to content

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:

  1. Mark the user-facing strings in your source
  2. Use the i18n function if needed
  3. Coordinate with translators to create one or more translation data files (.po, “portable” object files)
  4. 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 for gettext(): 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:

  1. The label of the menu item of the plugin
  2. Labels on widgets in a dialog for the plugin
  3. Error and information messages to the user
Do not confuse the _() 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:

C
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:

C
      g_set_error (&error, GIMP_PLUG_IN_ERROR, 0,
                   _("Procedure '%s' works with zero or one layer."),
                   PLUG_IN_PROC);
Our Coding Style documents some good practices when marking strings for localization.

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:

Python
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:

Scheme
SF-OPTION     _"Orientation"    '(_"Horizontal" _"Vertical")
There is no 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 locale subdirectory 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.mo

then 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():

C
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 disabled

You can implement the method and simply refuse localization, and the warning goes away:

C
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:

Python
def do_set_i18n(self, procedure_name):
   return False, None, None

Those 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:

Scheme
(script-fu-register-i18n "PDB name"
                          "domainName" | "Standard" | "None"
                          ["relative path"]) => void

That function takes two or three arguments and returns nothing:

  1. The first argument is of type string and is the name of a PDB procedure;
  2. 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”;
  3. 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
For a Python plug-in, the only difference would be telling which language it is parsing by adding -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.pot

A 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.po

If 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:

meson.build
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 tells xgettext about the _() and N_() markers, and about which of our functions take format strings (g_set_error() and friends);
  • install_dir is the locale directory 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.mo

Duplicating 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.

This is also why putting related procedures in a single plug-in directory, with a single domain, is the simplest solution of all.

Conclusion⁠

As you see, internationalizing a plug-in is a quick 4-step affair.

Last updated on