-
Notifications
You must be signed in to change notification settings - Fork 0
document translation marking for developers
The darktable program contains a lot of text; labels, tooltips, dialogs etc etc. To allow translators to provide translations for hardcoded text in the program and for those translations to be used at run-time when showing the text to the user, it is necessary that those string literals are marked and converted in the code. Depending on the context, this needs to be done in different ways, so that the build process picks up those strings and collects them in .po(t) files, where the translation team picks them up.
This is a string literal: "some text". Or maybe with some formatting codes to insert values: "an integer %d, a float %f and a string %s". When writing to the command line, using dt_print, we leave it as is, since that needs to be all in English.
When directly using it in a gtk function that will show it on screen, we mark it for translation and immediately translate it. This is done with the _() macro. For example you can say widget = gtk_label_new(_("label text"));
If you want to store the original string (because you may need the English version at some point), but mark it for translation so you'll be able to convert it later, you use N_(). For example const gchar *label = N_("label text"); widget = gtk_label_new(_(label));. Many darktable functions require marked but untranslated strings, because they internally use English but also need to show to the screen. For example dt_bauhaus_widget_set_label(g->contrast, NULL, N_("contrast"));. You can assign N_ marked strings to constant arrays, for example, which is not possible with translated strings.
When providing labels for iop parameter fields or enum values using the introspection markup $DESCRIPTION: the strings are always implicitly marked for translation, so nothing needs to be added:
float quantization; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 0.0 $DESCRIPTION: "mask quantization"
Sometimes the translation of a word depends on the context it is used in. For example "look" as a verb might in some languages translate differently from "a look". _() and N_() have variants that allow specifying the context; C_("verb", "look") and NC_("verb", "look"). In introspection $DESCRIPTIONs, you can use the Gnome way of adding the context to a string with "|". So $DESCRIPTION: "verb|look". As an aside, N_() in darktable uses this format internally to pass string and context to functions. The standard N_ would just drop the context, so the receiving function would have to assume it was always the same, requiring the same translation to be provides for many different "contexts", just in case one language would differentiate. A string that contains its own context can be translated with Q_(), so instead of C_("verb", "look") you could use Q_("verb|look"). There's no direct equivalent to NC_ for Q_, so to mark a string for translation with a context and use it elsewhere you'd use for example const gchar *label = NC_("verb", "look"); widget = gtk_label_new(Q_(label));. There is an NQ_() macro but it just strips the context from the string; NQ_("verb|look") == "look".
The directory data/styles contains camera specific default styles that have a property that can be localised. It can also be "segmented" (i.e. have multiple levels, which are separated by a "|" character. Yes, unfortunately this is the same character Gnome uses to separate the context). Commonly, the brand and model of the camera should not be localised, but some other parts of the name might. So each part that should be marked for translation must be tagged with the "l10n" prefix. An example would be
<name>_l10n_darktable|_l10n_camera styles|Canon|_l10n_generic</name>
When creating user defined styles, the same segmentation and translation markers can be used ("|" and "l10n") but only words that were already known at compile time will actually have a translation. So in "_l10n_generic|_l10n_favorites" the first word will likely continue to work, but the second currently will not.
The same goes for user-defined preset names. However, predefined presets (for iop modules) are not read from xml files, but set up in the code with calls to dt_gui_presets_add_generic. These take a name marked for translation using _("") and they also can contain "|" separators for "segmentation" (grouping/multi-levels). Internally, each name is prefixed with "builtin" so that we know it needs to be converted when showing to the user (but not when saving the preset itself or a shortcut to it, because this would otherwise break if the user switches the application language in preferences). You cannot use contexts in builtin presets names and the whole name, including separators, will be translated as one unit. If multiple segments are used, that makes it unlikely that two identical names coexist that would require different translations, so contexts are not really necessary. But it does mean that identical parts of the names needs to be repeatedly translated by the translation team each time. They are also responsible for keeping the same number of segments (although they could decide on a language-by-language basis to completely change the presets hierarchy for a module).
Each of the use cases of translatable strings mentioned above have many examples in the code, so it is probably easiest, when implementing a new module or functionality, to look at similar code and copy what markings are used there, paying close attention to the use of "N_()" vs "_()" (and adding a C before the underscore and "context", if a translator indicates they need it).
If all translatable strings have been marked correctly before passing them to functions and if all the translations are up to date and if all the parts of the infrastructure that need to be aware of the different types of translatable/segmented strings handle them correctly, all will be dandy. But a common problem is that strings are translated too early (using _() instead of N_()). This will not be immediately obvious to an inexperienced developer, because they will still see a translated string in the UI as expected (at least, as soon as the translator has provided one; before that happens, the developer can't really distinguish between "translated" and "untranslated" anyway). But problems can arise when internally the untranslated string is still required. This is for example the case for shortcuts. If those get translated too early, it means that key bindings will no longer connect if the program language gets switched in preferences, because the previously existing translated action can no longer be found. LUA code could also break or become language specific. So it is essential that functions like dt_action_define(_iop) (and DT_IOP_SECTION_FOR_PARAMS, dt_bauhaus_widget_set_label, dt_iop_(toggle)button_new etc.) get called using N_() marked names both for the label and the section.
When the infrastructure is enhanced to deal with new functional requirements, it is a frequent occurrence that things break elsewhere. The developer working on a particular area now might not always consider (or be aware of) all the other areas that could be impacted by core changes. Or even know how those are supposed to work. The below is trying to be an exhaustive list of all the things that need to be tested for breakage. If something you've worked on/fixed at some point is missing, please add it to the end of the list (so the numbering does not change).
Before testing, go into preferences:
- In general, set the user interface language to French
- In shortcuts import this file
7=iop/colorbalancergb/preset/_builtin_basic colorfulness | natural skin 8=lib/metadata_view/preset/_l10n_single|_l10n_maker 9=global/styles/_l10n_darktable|_l10n_camera styles|Canon|_l10n_generic - Close preferences
- In the hamburger/preferences of the "image information" module untick the first item (pellicule)
- Save as new preset (hamburger/Nouveau Prereglage) with name "_l10n_single|_l10n_maker"
- Close darktable (and wait till the process has ended)
- Restart
Go to darkroom, module "Balance de couleur RVB"
-
In the presets/hamburger menu there's a presets submenu "Couleur de base" which contains (at least) four entries. One is "Peau naturelle"
-
Press the module reset button. Press "7". The toast should say:
Balance de colour RVB: Applique le préréglage « Couleur de base | Peau naturelle » -
The title of the module will now also include the preset name
Balance de colour RVB . Couleur de base | Peau naturelle -
Click on the presets (hamburger) button. The submenu "Couleur de base" should now be highlighter. Click on it. "Peau naturelle" should be highlighted and have a tick mark.
-
Press "7" again. The first line of the toast now reads:
Balance de colour RVB Couleur de base | Peau naturelle : -
Move any of the sliders in the module. Create a new preset (hamburger/Nouveau préréglage). Paste name "Couleur de base|_l10n_debug| _l10n_draft | _l10n_lossy". Check that in the presets menu the user presets tree is not merged with the built-in presets tree and that all 4 levels of the subtree are translated.
-
Move any of the sliders in the module. Open the presets/hamburger menu and check that "Mettre a jour le préréglage" item at the bottom shows the fully translated user preset name.
-
Click on the shortcut mapping icon (next to preferences) and click on the header of Balance de colour RVB. The shortcuts/Raccourcis dialog opens. The bottom half should (at least) show
Dans la vue active 7 Modules de traitement/Balance de colour RVB/Préréglage/Couleur de base | Peau naturelle -
Select this shortcut. Press ctrl+V. Pasting in an editor should give:
dt.gui.action("iop/colorbalancergb/preset/_builtin_basic colorfulness | natural skin", 1,000, 0) -
In the top half of the shortcuts dialog, click on "Balance de colour RVB" Press shift+right arrow to expand the whole hierarchy of actions related to the module. It should look like below.
-
Click on Decalage/Teinte. Press ctrl+V. Pasting in an editor should give:
dt.gui.action("iop/colorbalancergb/offset/hue", "value", "edit", 1,000, 0) -
Type (without the quotes) "power epaule"[ENTER]. This should find the slider under AgX. Check that its section is translated to "Paramètres basique de la courbe". Press ctrl+V. Pasting in an editor should give: dt.gui.action("iop/agx/basic curve parameters/shoulder power", "value", "edit", 1,000, 0)
-
Check that in export module, there is no subtree for "actionbutton" and that "start export" has been translated.
-
In the Preferences/Préréglages tab, find and expand "Balance de colour RVB". Check that all presets have been translated. For example "Couleur de base | Peau naturelle"
-
In darkroom, press the bottom left button "Accès rapide aux préréglages" and then "Gérer la liste des préréglages…". Expand "Balance de colour RVB" and select "Couleur de base | Peau naturelle"
-
Press "Accès rapide aux préréglages" again and see it now has an entry for "Peau naturelle". The name includes the full current module name, so it might include the preset name twice if it is currently active. For example "Balance de colour RVB Couleur de base | Peau naturelle Couleur de base | Peau naturelle"
-
Press the second button from bottom left "Accès rapide aux styles". Check that submenus are correctly generated and translated. For example 4 levels for "Darktable > Styles de boîtier > Canon > Générique"
-
Press 9. Toast should show "Le style « Darktable > Styles de boîtier > Canon > Générique » est appliqué à l’image actuelle"
-
Check in Tagging/Mots-cles module that the tag "darktable|style|Darktable|Styles de boîtier|Canon|Générique" has been translated and doesn't contain spaces around the |. You may have to toggle the "checkmark" button to see internal darktable tags.
-
In the Styles module (by default only available in Lighttable) check that the same hierarchy is correctly displayed in a tree and translated.
-
Expand Darktable > Styles de boîtier > Canon and select Générique. Click button "Éditer". Check that the title of the dialog "Modifier le style" contains the translated full style name. In the name field underneath, the style name is not translated, because here the user can add (or remove) the
_l10n_tags to mark segments of the name for translation. All the modules listed in the dialog should be translated (including any added active preset names) -
Expand the "Information de l'image" module. Press its reset button. Press 8. The first item (pellicule) in the view should now be hidden.
-
Make a change to the module settings (via presets("hamburger")/Preferences..., remove another item from the list). Open the presets menu again. You should now see "Mettre a jour le préréglage single|Fabrikant". Check it is translated.