Internationalization and Localization
gtkmm applications can easily support multiple languages, including non-European languages such as Chinese and right-to-left languages such as Arabic. An appropriately-written and translated gtkmm application will use the appropriate language at runtime based on the user's environment.
You might not anticipate the need to support additional languages, but you can never rule it out. And it's easier to develop the application properly in the first place rather than retrofitting later.
The process of writing source code that allows for translation is called internationalization, often abbreviated to i18n. The Localization process, sometimes abbreviated as l10n, provides translated text for other languages, based on that source code.
The main activity in the internationalization process is finding strings seen by users and marking them for translation. You do not need to do it all at once - if you set up the necessary project infrastructure correctly then your application will work normally regardless of how many strings you've covered.
String literals should be typed in the source code in English, but surrounded by a macro. The gettext (or intltool) utility can then extract the marked strings for translation, and substitute the translated text at runtime.

Preparing your project
In the instructions below we will assume that you will not be using gettext directly, but intltool, which was written specifically for GNOME. intltool uses gettext(), which extracts strings from source code, but intltool can also combine strings from other files, for example from desktop menu details, and GUI resource files such as Glade files, into standard gettext .pot/.po files.
We also assume that you are using autotools (e.g. automake and autoconf) to build your project, and that you are using ./autogen.sh from gnome-common, which, among other things, takes care of some intltool initialization.
Create a sub-directory named po in your project's root directory. This directory will eventually contain all of your translations. Within it, create a file named LINGUAS and a file named POTFILES.in. It is common practice to also create a ChangeLog file in the po directory so that translators can keep track of translation changes.
LINGUAS contains an alphabetically sorted list of codes identifying the languages for which your program is translated (comment lines starting with a # are ignored). Each language code listed in the LINGUAS file must have a corresponding .po file. So, if your program has German and Japanese translations, your LINGUAS file would look like this:

# keep this file sorted alphabetically, one language code per line
de
ja

(In addition, you'd have the files ja.po and de.po in your po directory which contain the German and Japanese translations, respectively.)
POTFILES.in is a list of paths to all files which contain strings marked up for translation, starting from the project root directory. So for example, if your project sources were located in a subdirectory named src, and you had two files that contained strings that should be translated, your POTFILES.in file might look like this:

src/main.cc
src/other.cc

If you are using gettext directly, you can only mark strings for translation if they are in source code file. However, if you use intltool, you can mark strings for translation in a variety of other file formats, including Glade UI files, xml, .desktop files and several more. So, if you have designed some of the application UI in Glade then also add your .glade files to the list in POTFILES.in.
Now that there is a place to put your translations, you need to initialize intltool and gettext. Add the following code to your configure.ac, substituting 'programname' with the name of your program:

IT_PROG_INTLTOOL([0.35.0])

GETTEXT_PACKAGE=programname
AC_SUBST(GETTEXT_PACKAGE)
AC_DEFINE_UNQUOTED([GETTEXT_PACKAGE], ["$GETTEXT_PACKAGE"],
                   [The domain to use with gettext])
AM_GLIB_GNU_GETTEXT

PROGRAMNAME_LOCALEDIR=[${datadir}/locale]
AC_SUBST(PROGRAMNAME_LOCALEDIR)

This PROGRAMNAME_LOCALEDIR variable will be used later in the Makefile.am file, to define a macro that will be used when you initialize gettext in your source code.
In the top-level Makefile.am:
Add po to the SUBDIRS variable. Without this, your translations won't get built and installed when you build the program
Define INTLTOOL_FILES as:

INTLTOOL_FILES = intltool-extract.in \
                 intltool-merge.in \
                 intltool-update.in

Add INTLTOOL_FILES to the EXTRA_DIST list of files. This ensures that when you do a make dist, these commands will be included in the source tarball.
Update your DISTCLEANFILES:

DISTCLEANFILES = ... intltool-extract \
                 intltool-merge \
                 intltool-update \
                 po/.intltool-merge-cache

In your src/Makefile.am, update your AM_CPPFLAGS to add the following preprocessor macro definition:

AM_CPPFLAGS = ... -DPROGRAMNAME_LOCALEDIR=\"${PROGRAMNAME_LOCALEDIR}\"

This macro will be used when you initialize gettext in your source code.

Marking strings for translation
String literals should be typed in the source code in English, but they should be surrounded by a call to the gettext() function. These strings will be extracted for translation and the translations may be used at runtime instead of the original English strings.
The GNU gettext package allows you to mark strings in source code, extract those strings for translation, and use the translated strings in your application.
However, Glib defines gettext() support macros which are shorter wrappers in an easy-to-use form. To use these macros, include <glibmm/i18n.h>, and then, for example, substitute:

display_message("Getting ready for i18n.");

with:

display_message(_("Getting ready for i18n."));

For reference, it is possible to generate a file which contains all strings which appear in your code, even if they are not marked for translation, together with file name and line number references. To generate such a file named my-strings, execute the following command, within the source code directory:

xgettext -a -o my-strings --omit-header *.cc *.h

Finally, to let your program use the translation for the current locale, add this code to the beginning of your main.cc file, to initialize gettext.

bindtextdomain(GETTEXT_PACKAGE, PROGRAMNAME_LOCALEDIR);
bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
textdomain(GETTEXT_PACKAGE);

How gettext works
intltool / xgettext script extracts the strings and puts them in a mypackage.pot file. The translators of your application create their translations by first copying this .pot file to a localename.po file. A locale identifies a language and an encoding for that language, including date and numerical formats. Later, when the text in your source code has changed, the msmerge script is used to update the localename.po files from the regenerated .pot file.
At install time, the .po files are converted to a binary format (with the extension .mo) and placed in a system-wide directory for locale files, for example /usr/share/locale/.
When the application runs, the gettext library checks the system-wide directory to see if there is a .mo file for the user's locale environment (you can set the locale with, for instance, "export LANG=de_DE.UTF-8" from a bash console). Later, when the program reaches a gettext call, it looks for a translation of a particular string. If none is found, the original string is used.

Testing and adding translations
To convince yourself that you've done well, you may wish to add a translation for a new locale. In order to do that, go to the po subdirectory of your project and execute the following command:

intltool-update --pot

That will create a file named programname.pot. Now copy that file to languagecode.po, such as de.po or hu.po. Also add that language code to LINGUAS. The .po file contains a header and a list of English strings, with space for the translated strings to be entered. Make sure you set the encoding of the .po file (specified in the header, but also as content) to UTF-8.
It's possible that certain strings will be marked as fuzzy in the .po file. These translations will not substitute the original string. To make them appear, simply remove the fuzzy tag.

Resources
More information about what lies behind the internationalization and localization process is presented and demonstrated in:
L10N Guidelines for Developers
Intltool README
How to use Git for GNOME translators
gettext manual
gtkmm_hello example package
gnomemm_hello example package

Expecting UTF8
A properly internationalized application will not make assumptions about the number of bytes in a character. That means that you shouldn't use pointer arithmetic to step through the characters in a string, and it means you shouldn't use std::string or standard C functions such as strlen() because they make the same assumption.
However, you probably already avoid bare char* arrays and pointer arithmetic by using std::string, so you just need to start using Glib::ustring instead. See the Basics chapter about Glib::ustring.

Glib::ustring and std::iostreams
Unfortunately, the integration with the standard iostreams is not completely foolproof. gtkmm converts Glib::ustrings to a locale-specific encoding (which usually is not UTF-8) if you output them to an ostream with operator<<. Likewise, retrieving Glib::ustrings from istream with operator>> causes a conversion in the opposite direction. But this scheme breaks down if you go through a std::string, e.g. by inputting text from a stream to a std::string and then implicitly converting it to a Glib::ustring. If the string contained non-ASCII characters and the current locale is not UTF-8 encoded, the result is a corrupted Glib::ustring. You can work around this with a manual conversion. For instance, to retrieve the std::string from a ostringstream:

std::ostringstream output;
output.imbue(std::locale("")); // use the user's locale for this stream
output << percentage << " % done";
label->set_text(Glib::locale_to_utf8(output.str()));

Pitfalls
There are a few common mistakes that you would discover eventually yourself. But this section might help you to avoid them.

Same strings, different semantics
Sometimes two english strings are identical but have different meanings in different contexts, so they would probably not be identical when translated. Since the English strings are used as look-up keys, this causes problems.
In these cases, you should add extra characters to the strings. For instance, use "jumps[noun]" and "jumps[verb]" instead of just "jumps" and strip them again outside the gettext call. If you add extra characters you should also add a comment for the translators before the gettext call. Such comments will be shown in the .po files. For instance:

// note to translators: don't translate the "[noun]" part - it is
// just here to distinguish the string from another "jumps" string
text = strip(gettext("jumps[noun]"), "[noun]");

Composition of strings
C programmers use sprintf() to compose and concatenate strings. C++ favours streams, but unfortunately, this approach makes translation difficult, because each fragment of text is translated separately, without allowing the translators to rearrange them according to the grammar of the language.
For instance, this code would be problematic:

std::cout << _("Current amount: ") << amount
          << _(" Future: ") << future << std::endl;

label.set_text(_("Really delete ") + filename + _(" now?"));

So you should either avoid this situation or use Glib::ustring::compose() which supports syntax such as:

std::cout << Glib::ustring::compose(
             _("Current amount: %1 Future: %2"), amount, future) << std::endl;

label.set_text(Glib::ustring::compose(_("Really delete %1 now?"), filename));

Assuming the displayed size of strings
You never know how much space a string will take on screen when translated. It might very possibly be twice the size of the original English string. Luckily, most gtkmm widgets will expand at runtime to the required size.

Unusual words
You should avoid cryptic abbreviations, slang, or jargon. They are usually difficult to translate, and are often difficult for even native speakers to understand. For instance, prefer "application" to "app"

Using non-ASCII characters in strings
Currently, gettext does not support non-ASCII characters (i.e. any characters with a code above 127) in source code. For instance, you cannot use the copyright sign (©).
To work around this, you could write a comment in the source code just before the string, telling the translators to use the special character if it is available in their languages. For english, you could then make an American English en_US.po translation which used that special character.

Getting help with translations
If your program is free software, there is a whole GNOME subproject devoted to helping you make translations, the GNOME Translation Project.
The way it works is that you upload your source code to a git repository where translators can access it, then contact the gnome-i18n mailing list and ask to have your program added to the list of modules to translate.
Then you make sure you update the file POTFILES.in in the po/ subdirectory (intltool-update -M can help with this) so that the translators always access updated myprogram.pot files, and simply freeze the strings at least a couple of days before you make a new release, announcing it on gnome-i18n. Depending on the number of strings your program contains and how popular it is, the translations will then start to tick in as languagename.po files.
Note that most language teams only consist of 1-3 persons, so if your program contains a lot of strings, it might last a while before anyone has the time to look at it. Also, most translators do not want to waste their time (translating is a very time-consuming task) so if they do not assess your project as being really serious (in the sense that it is polished and being maintained) they may decide to spend their time on some other project.