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.@__str
— Macro_"..."
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.
Gettext.@gettext
— Macro@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.
Gettext.@ngettext
— Macro@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.
Gettext.@pgettext
— Macro@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.
Gettext.@npgettext
— Macro@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.
Gettext.@N__str
— MacroN_"..."
"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.)
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:
- Call
bindtextdomain
to specify the path of thepo
directory containing translations for that domain. This is typically done in a module's__init__
function (see below). - 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 withtextdomain
for code running in Julia'sMain
module (scripts and interactive work).
Gettext.bindtextdomain
— Functionbindtextdomain(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.)
Gettext.textdomain
— Functiontextdomain([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.
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.gettext
— Functiongettext([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.
Gettext.ngettext
— Functionngettext([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.
Gettext.pgettext
— Functionpgettext([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.
Gettext.npgettext
— Functionnpgettext([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.