Internationalization (i18n) API

Marking strings for translation

The most basic i18n task is to mark every user-visible string for possible translation. This is typically done with the macros describe in the following subsections.

(Note: we use macros, rather than functions, so that they can automatically substitute the current __GETTEXT_DOMAIN__ when used in a module.)

See also the GNU gettext manual's tips on how to prepare translatable strings in your code. In short, they recommend translating entire English sentences/paragraphs (avoiding forming sentences by concatenation), splitting at paragraphs, with placeholders for interpolation/substitution (see below).

Simple literal strings: _"..."

For most literal strings, you can simply replace "..." with _"...", i.e. prepend an underscore.

_"..." acts just like an ordinary Julia literal string, but internally it corresponds to a call to @gettext("..."), returning a translated string if appropriate (assuming a translation exists for the current locale). The only other big difference from a typical Julia string is that $ interpolation is not supported in _"..." (any $ is treated literally). This is intentional: translation strings should not depend on runtime values, because a .po file contains only a finite number of translations, so runtime interpolation should be employed judiciously as described below.

Interpolating into translated strings

If you need to substitute another value into a translation, the standard practice is to put a a printf-like placeholder (e.g. %s or %d) into the string, and then substitute it after translation.

For example, suppose that your program outputs the string "Congratulations! You won $prize!", where $prize substitutes some other Julia string prize, like prize="a yacht" or prize="$100. To i18n this string, one strategy might be:

replace(_"Congratulations!  You won %s!", "%s"=>@gettext(prize))

This way, it looks up a single string "Congratulations! You won %s!" in the .po file, and the translation should also have the %s placeholder. (For example, the Spanish translation might be "¡Felicidades, ganaste %s!".) Here, we also call @gettext to look up the translation (if any) for the runtime value of prize — presumably, there might be translations for a finite number of cases like "a yacht", while other strings like $100 could be left as-is.

(The potential danger of string substitution like in this example is that some languages may require you to change the surrounding text, e.g. to change the verb form, depending on the interpolated words. It is always better to translate complete sentences if possible.)

Here, we are using Julia's built-in replace function. To perform more complicated string-formatting substitutions, one could instead use the Printf standard library, or perhaps Python-style format strings via the Format.jl package.

Interpolating runtime singular or plural forms: @ngettext

What if you want to translate a string like "Your birthday is in $n days.", where if n == 1 it should instead use a singular noun? (And some languages might have a specialized plural for n == 2 as well.)

In this case, a simple placeholder for n is not enough. Instead, you can use the macro @ngettext, which allows separate singular and plural translations:

@ngettext("Your birthday is in %d day.", "Your birthday is in %d days.", "%d"=>n)

Here, we provide both singular and plural forms of the string to be translated, and @ngettext will choose one based on the runtime value of n. (In fact, for some languages, gettext may choose among multiple plural forms.) The translation strings should also have a %d placeholder, and the "%d"=>n argument tells @ngettext to substitute string(n) for %d in the final result (using replace).

If you want to perform more complicated numeric formatting on your own, e.g. with Printf, you can pass n instead of "%d"=>n and no substitution will be performed on the result.

Providing additional translation context: @pgettext

Sometimes, the same string might be used in different contexts in a program that require different translations. This is especially common for very short strings (e.g. single words). For example, the string "Open" in a File menu might be translated into Spanish as a verb "Abrir" (to open a file), but the same string might be translated as an adjective "Abierto" to indicate that a door is open in a game.

To support this case, Gettext allows you to pass an additional context string for a translation, using the macro @pgettext. For example:

@pgettext("File menu", "Open")  # translate "Open" in the File menu
@pgettext("Door", "Open")       # translate an "Open" sign attached to a door

Similarly, there is a macro @npgettext that is like @ngettext but has an additional context string as the first argument.

Macro reference

The following are the string-i18n macros:

Gettext.@__strMacro
_"..."

Returns the translation (if any) for the given literal string "..." via @gettext.

This string can contain backslash escapes like ordinary Julia literal strings, but $ is treated literally (not used for interpolations): translation strings should not generally contain runtime values.

source
Gettext.@gettextMacro
@gettext(msgid::AbstractString)

Look up the translation (if any) of msgid, returning the translated string, or returning msgid if no translation was found.

In a module != Main, this passes the module's __GETTEXT_DOMAIN__ as the domain argument to gettext, whereas the global textdomain is used in the Main module.

source
Gettext.@ngettextMacro
@ngettext(msgid::AbstractString, msgid_plural::AbstractString, n::Integer)
@ngettext(msgid::AbstractString, msgid_plural::AbstractString, nsub::Pair{<:AbstractString, <:Integer})

Look up the translation (if any) of msgid, with the plural form given by msgid_plural, returning the singular form if n == 1 and a plural form if n != 1 (n must be nonnegative), giving a translated string if available.

Instead of passing an integer n, you can pass a Pair placeholder=>n, in which case case the string placeholder is replaced by n in the returned string; most commonly, placeholder == "%d" (in printf style). (Note that this is a simple string replacement; if you want more complicated printf-style formating like "%05d" then you will need to call a library like Printf yourself.)

In a module != Main, this passes the module's __GETTEXT_DOMAIN__ as the domain argument to ngettext, whereas the global textdomain is used in the Main module.

source
Gettext.@pgettextMacro
@pgettext([domain::AbstractString], context::AbstractString, msgid::AbstractString)

Like @gettext, but also supplies a context string for looking up msgid, returning the translation (if any) or msgid (if no translation was found).

In a module != Main, this passes the module's __GETTEXT_DOMAIN__ as the domain argument to pgettext, whereas the global textdomain is used in the Main module.

source
Gettext.@npgettextMacro
@npgettext(context::AbstractString, msgid::AbstractString, msgid_plural::AbstractString, n::Integer)
@npgettext(context::AbstractString, msgid::AbstractString, msgid_plural::AbstractString, nsub::Pair{<:AbstractString, <:Integer})

Like @ngettext, but also supplies a context string for looking up msgid or its plural form msgid_plural (depending on n), optionally performing a text substitution if a Pair is passed for the final argument.

In a module != Main, this passes the module's __GETTEXT_DOMAIN__ as the domain argument to npgettext, whereas the global textdomain is used in the Main module.

source
Gettext.@N__strMacro
N_"..."

"No-op" translation, equivalent to "...", for strings that do not require translation.

This string can contain backslash escapes like ordinary Julia literal strings, but $ is treated literally (not used for interpolations).

(The main use of this macro is to explicitly mark strings to ensure that they are excluded from automated translation tools.)

source

Domains

Every translation in Gettext is relative to a "domain", which usually corresponds to a single program or package. Each domain has a list of strings to be translated, and can have domain.po files that give translations for particular locales (see Localization (l10n) and PO files). To control the domain being used, you need to do two things:

  1. Call bindtextdomain to specify the path of the po directory containing translations for that domain. This is typically done in a module's __init__ function (see below).
  2. Specify the domain you are using: this is done via the __GETTEXT_DOMAIN__ constant in modules/packages (below), or is done by setting a global domain with textdomain for code running in Julia's Main module (scripts and interactive work).
Gettext.bindtextdomainFunction
bindtextdomain(domain::AbstractString, [path::AbstractString])

Specify that the po directory for domain is at path (if supplied), returning the current (absolute) path for domain.

(If this function is not called, then gettext will look in a system-specific directory like /usr/local/share/locale for translation catalogs.)

source
Gettext.textdomainFunction
textdomain([domain::AbstractString])

Set the global Gettext domain to domain (if supplied), returning the current global domain.

This domain is used for calls to low-level functions like gettext when no domain argument is passed, and also for macros like _"..." and @getext when used from the Main module.

source

See also Locating Message Catalog Files in the GNU gettext manual.

Gettext for modules and packages

To use Gettext.jl in a module MyModule, especially in a Julia package, at the beginning of your module you should define a const __GETTEXT_DOMAIN__ to a unique domain name for your package's translations, and in your top-level module's __init__ function you should call bindtextdomain to specify the path to your package's po directory (see Localization (l10n) and PO files), typically at the top level of your package.

It should look something like this:

module MyModule

using Gettext
const __GETTEXT_DOMAIN__ = "MyModule-<uuid>" # replace with package UUID
function __init__()
    bindtextdomain(__GETTEXT_DOMAIN__, joinpath(@__DIR__, "..", "po"))
end

# ...module implementation...

end

In "MyPackage-<uuid>", the <uuid> denotes the unique UUID identifier of your package — this ensures that two packages will not have the same gettext domain, even if they happen to have the same name.

When they are used in any module (other than Julia's implicit Main module for scripts and interactive work), the macros, _"...", @gettext, and so on (see above) pass this global variable __GETTEXT_DOMAIN__ to the corresponding low-level functions. You will get an UndefVarError if you use those macros in a module that does not define __GETTEXT_DOMAIN__. (In the Main module, the same macros instead use the global textdomain.)

The bindtextdomain call in the example above assumes that you have a top-level directory po in your package, which is a good default location. This directory is used to store translation (.po) files po/<locale>/LC_MESSAGES/MyModule-<uuid>.po, where <locale> is the standard locale identifier, e.g. en (English) or en_GB (English, Great Britain), and MyModule-<uuid> is your domain name from above.

If your package has submodules, in most cases they can simply employ the same domain as your top-level module MyModule, via:

using Gettext
using ..MyModule: __GETTEXT_DOMAIN__`

Lower-level API

The following lower-level API functions can be used instead of the macros for the rare cases in which you want more manual control over the Gettext domain:

Gettext.gettextFunction
gettext([domain::AbstractString], msgid::AbstractString)

Look up the translation (if any) of msgid in domain (if supplied, or in the global textdomain otherwise), returning the translated string, or returning msgid if no translation was found.

See also @gettext to use the domain of the current module.

source
Gettext.ngettextFunction
ngettext([domain::AbstractString], msgid::AbstractString, msgid_plural::AbstractString, n::Integer)
ngettext([domain::AbstractString], msgid::AbstractString, msgid_plural::AbstractString, nsub::Pair{<:AbstractString, <:Integer})

Look up the translation (if any) of msgid in domain (if supplied, or in the global textdomain otherwise), with the plural form given by msgid_plural, returning the singular form if n == 1 and a plural form if n != 1 (n must be nonnegative), giving a translated string if available.

Instead of passing an integer n, you can pass a Pair placeholder=>n, in which case case the string placeholder is replaced by n in the returned string; most commonly, placeholder == "%d" (in printf style). (Note that this is a simple string replacement; if you want more complicated printf-style formating like "%05d" then you will need to call a library like Printf yourself.)

See also @ngettext to use the domain of the current module.

source
Gettext.pgettextFunction
pgettext([domain::AbstractString], context::AbstractString, msgid::AbstractString)

Like gettext, but also supplies a context string for looking up msgid (in domain, if supplied, or the global textdomain otherwise), returning the translation (if any) or msgid (if no translation was found).

See also @pgettext to use the domain of the current module.

source
Gettext.npgettextFunction
npgettext([domain::AbstractString], context::AbstractString, msgid::AbstractString, msgid_plural::AbstractString, n::Integer)
npgettext([domain::AbstractString], context::AbstractString, msgid::AbstractString, msgid_plural::AbstractString, nsub::Pair{<:AbstractString, <:Integer})

Like ngettext, but also supplies a context string for looking up msgid or its plural form msgid_plural (depending on n), optionally performing a text substitution if a Pair is passed for the final argument.

See also @npgettext to use the domain of the current module.

source