Making sure we have good translations takes a lot of work during development. There's several techniques to keep in mind. In addition, the most critical thing to remember is that each release of a product has a string change deadline. After this point, we should not make any changes to strings to allow the translators time to work. We can make exceptions, but it takes coordination.
What not to translate
We do not mark log messages for translation. We are the most frequent viewers of log files via bug reports, so translated logs just makes our job harder.
Adding new files
If you add a new file with translatable text (source code or glade files), you need to add the file name to po/POTFILES.in in the correct place. This file gets passed to the tools that build the anaconda.pot file. Any strings in files not mentioned here will be left out, which means they do not get worked on by translators.
Marking strings for translation
If you have a user-visible string, it should be marked for translation. There are three commonly used functions to do this, which are conveniently defined in pyanaconda/i18n.py:
_ = lambda x: gettext.ldgettext("anaconda", x)
This function both marks a string for translation so the gettext tools will include it in anaconda.pot, and causes the string argument to be replaced by its translation at this point in the code. This is the most commonly used translation marking function.
N_ = lambda x: x
This function obviously does nothing in anaconda. However, it does also mark a string for translation and inclusion in anaconda.pot. When you later want to display the translated text, you will need to use _. This function is seldom used.
P_ = lambda x, y, z: gettext.ldngettext("anaconda", x, y, z)
Finally, this function works just like _ but correctly handles plurals. The rules for how to display the singular or plural of a string vary wildly between languages, but gettext somehow knows how to figure it all out. All you have to do is provide the singular string, the plural string, and the number. We use this one to display variations like "1 disk selected" or "2 disks selected" instead of the much lamer and lazier "1 disk(s) selected".
Global strings
Sometimes, you may want to use the same string in several places. Naturally, this means you will want to define it as a global variable outside of any class. If you do this, you must mark the string using the N_ function and then later display it with _.
This is because anaconda commonly starts up in English, but the user is free to change language at the welcome screen. We may import our python modules before this change occurs. If this happens, strings marked with _ outside of a class will be translated from English (since all our strings are in English in the source code) to English (the language in use at import time). The user then changes the runtime language but because the string has already been translated, it will appear in English.
A great example of avoiding this problem is in pyanaconda/ui/gui/spokes/custom.py. Note how unrecoverable_error_msg is defined with N_ at the top of the file, but is then wrapped as _(unrecoverable_error_msg) later on when it is displayed on the screen.
String substitutions
Substituting a value into a string is something we do a lot of. Consider the following:
msg = "An error occurred when running lvm: %s" % e
At first, you may think the string should be marked for translation like this:
msg = _("An error occurred when running lvm: %s" % e)
However, this won't do what you want. Remember how translations work. At package build time, gettext will build a database of all strings marked with one of the function described above. So, this string would get entered into that database alongside its various translations. So far, so good.
Runtime is where the problem occurrs. If "e" above is "I/O error", then that will get substituted in and gettext will look for the following string in its database:
"An error occurred when running lvm: I/O error"
But this string isn't in there, because at build time we entered the string without the substitution into the gettext database. So, substitutions must always be done outside of the _ call, like so:
msg = _("An error occurred when running lvm: %s") % e
This will substitute the error message into the translated string, not the source string.
Complex string substitutions
Consider the following string:
msg = _("Device %s (%s) needs %s more space to install %s") % (dev, fs, needed, productName)
This is really hard to translate, because the translators do not see the context this string is defined in. All they see is the raw text inside the quotes. They are likely translating into a language where the words are in a completely different order than English. Thus, they need more help to be able to know how to deal with this string.
Luckily, python allows us to do substitutions using a dictionary. This makes the string more self-documenting for the translators. Whenever you have a string with more than one substitution, it makes sense to use this format:
msg = _("Device %(deviceName)s (%(fileSystemType)s) needs %(neededSpace)s more space to " "install %(productName)s") % {"deviceName": dev, "fileSystemType": fs, "neededSpace": needed, "productName": productName}
Again, watch to make sure you do the substitution outside of the _ call.
Word wrapping
Whenever possible, do not hardcode newlines into any string that will be displayed in the GUI. Instead, leave it as one big line and set word wrap mode on the widget. Certain languages (like German) tend to have much longer strings. Hardcoding newlines makes it more difficult for GTK to get the word wrapping right. The failure mode for this is pretty weird: You will commonly see the the window displayed in the upper right hand corner of the screen, with various widgets going off the right side.
Exceptions
One of the more confusing areas is what to do with exception text.
First, you need to decide whether the exception text is ever going to be handled and displayed to the user or not. Note that "handled and displayed" does not mean the top-level all-else-has-failed exception handler we've got that displays a traceback to the user. If the text is not user-visible, then nothing needs to be done.
Then you need to decide what to do based upon where the text came from:
If it came from inside anaconda (when we raise our own exception with our own text) then the text should be marked for translation following the above rules. Note that for most exceptions we raise ourselves, the text is typically not important. We have defined a lot of very specific exception classes and handle them in such a way that the text commonly does not need to be translated.
If it came from some program we ran, we do not have much control over the translation. Because errors from programs we ran typically fall into the same category as log messages (more useful for us than for users), we do not want them translated. We enforce this with the augmentEnv function in pyanaconda/iutil.py.
But even if we did want the error translated, there's little we can do about it. Not all programs are translated fully, and for space reasons we do not include the translations for every program we use on the installation media.
Finally, the text could have come from some other library we call. We do more commonly want to display these errors (especially when they come from pykickstart, or cracklib for instance) and we have better control over making sure they are translated. Before doing any special work, try just displaying the error and seeing what happens. Many are already correctly translated.
If not, however, you need to use gettext directly to look up the translation. Our _ functions will not work here because of translation domains. Basically, each program has its own database of translated text. Strings contained in libparted, for instance, will not be in anaconda's database - they will be in parted's. You must therefore look them up in the right place, like so:
msg = gettext.ldgettext("parted", e)
Markup
The GUI uses Pango markup to modify how certain strings are displayed. Markup that is applied to an entire string should not be included in the string used for translation. For example:
<b>Error adding FCoE SAN.</b>
In this case, the <b> tags are not useful to the translator. Instead of using <b> to mark the string as bold, set the weight to "bold" in the Pango attribute list in the glade file. For strings in Python where setting Pango attributes is not convenient, translate the content and add the surrounding markup to the result. Something like this example from pyanaconda/ui/gui/spokes/lib/resize.py:
freeSpaceString = "<span foreground='grey' style='italic'>%s</span>" % escape_markup(_("Free space"))
Markup that is applied to only part of the string, on the other hand, should be translated.
"Your current <a href=\"\"><b>%(product)s</b> software selection</a> requires <b>%(total)s</b> of available space"
In this example, only certain parts of the sentence are contained in <b> and <a> tags, so it's up to the translator to decide where those should go.
Unfortunately, we can't mix Pango attribute lists and markup, so there are some cases where unnecessary markup has to be included in the translatable string.
<span size=\"small\"><b>Example:</b> squid.mysite.org:3128</span>
In this case, <b> is applied only to "Example:", so it must be specified with markup and included in the translatable string, but the applies to the entire string and is not useful to the translator. The <span> is included in this case because we have no other way of setting the size="small" attribute on the string.