This is partly for organization which, like HCL, use Domino Global Workbench to do translations of their Notes/Domino applications. But also, even if you do translations manually, or even if you don’t do them at all, it makes sense to learn good habits for creating applications in a way that makes translations simple, because it’s not that hard and you never know.
Rationale
Your applications are more valuable if they’re capable of being translated.
It’s also easier to maintain the user-facing text of an application, even if it’s not translated, when such texts are stored in a predictable way.
Errors in translation can be hard to catch, because the testers generally either don’t speak all the targeted languages, or else they speak the language but don’t know how the application works in its original language. If a translation error prevents a given control from appearing on the screen at all, they don’t know it was supposed to be there. This has happened. This has gone undetected for years.
So anything you can do to reduce the likelihood of error, will save a ton of money for bug fixing later.
This is hard in Domino
Notes/Domino was designed before the current best practices for translatability were developed and refined. The newer design elements, such as XPages, do a much better job of automatically separating translatable text from other strings and making it easy to produce a package for translators.
This is not to say that it’s a breeze with XPages either — all development environments require discipline around this. But there are a lot fewer automatic ways to manage and use string resources with “classic” design elements like forms, views, and script libraries.
General principles
Placement in the code
Where source code contains user-facing strings, they should be collected in a predictable place, preferably near the beginning of the code. Someone should be able to glance at a single screen and see all the texts that particular piece of code might produce.
In LotusScript, that generally means translatable strings should be grouped in the Declarations section of the module, using Private Const statements to define symbolic names for them.
Avoid fragments
In cases where a sentence contains variable information, it shouldn’t be split into multiple strings, because the translators will be presented with these strings separately. Not only may they not understand that they’re supposed to be part of the same sentence, it may not even be possible to produce a reasonable translation while leaving the parts in the same order.
For instance, consider this code:
result = "You may not exceed " & VEGMAX & " vegetable ingredients in a " & recipeCategory & " recipe." ' bad programmer, no dessert for you!
Let’s suppose the Spanish translator realizes the parts go together and decides this can best be translated as “Las recetas de la categoría X pueden contener como máximo Y ingredientes vegetales.” There’s no way to make that happen by just changing the texts of those three strings. The variables appear in different relative positions in the sentence. You’d have to rewrite the whole line — and that’s precisely the sort of change you don’t want to make in a translation.
Instead, we’d like the translator to get something like this:
Private Const ERR_TOOMANYVEG = |You may not exceed {maxveg} vegetable ingredients in a {categoryname} recipe.|
After translation, this would read:
Private Const ERR_TOOMANYVEG = |Las recetas de la categoría {categoryname} pueden contener como máximo {maxveg} ingredientes vegetales.|
Of course then the question arises how to make use of that string when it comes time to plug in the variable data. You can do this on an “ad hoc” basis using the Replace function in LotusScript, but this is a bit tiresome. For an alternative, open the CompareDBs.ntf template from Domino 12.0.1 server in Designer, and find the zprintf Script Library. This code uses number arguments (“{0}”) rather than descriptive terms like the above example, but allows for comments appended to the number arguments (“{0:maxveg}”), so you can still manage.
(Note: the zprintf style of argument is actually better, because you just know some translator will translate “categoryname”, breaking the replacement code that’s looking for that exact string. Zprintf handles that case.
In macro language, you just have to use @ReplaceSubstring, but that’s life.
Segregate translatable and non-translatable parts of strings
The most frequent bad example I’ve seen of this is where HTML appears in code with translatable text embedded.
Print "<p class='alert'>This is a result of too many cooks.</p>"
Part of this string needs to be translated. Part doesn’t. If an overzealous translator translates the word “class” or “alert”, this will be a problem. Believe me, these people do exist. I’ve seen the work of several of them.
One can hope the translator will recognize your markup language and do a sensible thing, but they aren’t engineers, so they might not, especially if it’s a custom or less common markup syntax. They might also miss subtleties such as certain characters needing to be “escaped” so they aren’t treated as markup syntax.
Of course, this last thing is also a problem in cases where the string is broken out as a separate resource. If it’s part of some HTML, or part of a formula, the translated version might contain characters that have special meaning in the context where you use them, or they might be incorrectly escaped (e.g. with the wrong number of backslashes). It’s better if the translator can be presented a string that’s like what would be seen on the screen, with minimal markup. Then where the string is used, any special characters can be swapped out for their escaped versions.
Private Const MSG_TOOMANYCOOKS = "This is the result of too many cooks." ... Print "<p class='alert'>" & ToHTML(MSG_TOOMANYCOOKS) & "</p>"
Of course, you need the function to do the translation — into HTML, or macro language, or JSON, or whatever the syntax of the markup language or source code you’re trying to generate. Again I refer you to the CompareDBs application, whose CommonUtil script library contains a few such functions.
Another case where we need to worry about translatable and nontranslatable text getting mixed up is in keyword lists. Code often contains strings of the form “name|value” where name should be translated and value shouldn’t. It’s hard to break these into separate strings, because they need to remain paired. If they’re not right together like this — if, let’s say, you have a list of names and then a separate list of the associated values — there’s a good chance someone maintaining the code will reorder or insert a value in one list without realizing they need to do the same to the other list.
So in many cases we found it more practical to send strings of the “name|value” format for translation and give the translators instructions to leave the value part alone. Most of them will follow that instruction. For the others, that’s why you need automated checkers for the property files the translators send back. This is an easy issue to detect automatically and flag for review, so you can catch it before it even gets merged into a template.
If you find this website helpful and want to give back, may I suggest buying, reading, and reviewing one of my excellent books? More are coming soon!
If you want email whenever there’s a new post, you can subscribe to the email list, which is 100% private and used only to send you information about stuff on this site.
Considerations specific to Domino Global Workbench
Domino Global Workbench (DGW) is a creaky old product that scans a Notes/Domino application and finds all potentially translatable strings in it, exporting those to “properties” files that can be sent to translators. The tool also has a function to import the translated files, merge them with the templates in the original language, and either create separate translated templates for each language, or single monster templates with all the languages jammed in together.
The trouble, of course, is that not all potentially translatable strings in an application should be translated. Many of them are internal values never shown to users, and need to have certain exact values or the code doesn’t work. It’s best to catch these potential overtranslations before they are sent off to the translators — even if they’re smart enough to realize the string shouldn’t be changed, they’re still going to charge you based on the amount of text you send them to translate.
DGW provides a few different ways for developers to specify which strings should be translated and which not.
DNT files
For all types of strings, there are “DNT” (do not translate) files for each application that list all the key values of strings that shouldn’t be translated. For a new application that hasn’t been translated before, it makes a lot of sense to page through all the generated properties files and find lines that obviously belong in the DNT file.
It can be hard to tell this seeing the string out of context — which is of course the same problem the translators will have. But at least you have the original application to refer back to in cases where it’s not obvious, which is something translators can’t do.
Stopwords
The NSF used to configure the operation of DGW contains a section where you can put in a list of words and prefixes that, if they match a candidate string, that string should not be listed for translation. This often includes brand names, the word Notes, etc.
Non-translate paragraph style
For static text on a form or subform, you’ll often have comment lines for developers that are set to hide during use. Or there might be bits of text you just know it doesn’t make sense to translate because they are a trademark or whatever.
There’s a special style name, I forget what it is, that if you create a saved paragraph style with that name and apply that style to a paragraph, DGW will ignore the text in that paragraph — it won’t be included in the properties files. This is only useful for text that’s not in a table, because you can’t apply a named style to text in a table. I don’t know about you, but in my case that’s little text that’s not in a table, so this method of tagging DNT text isn’t terribly useful for me.
A better convention is to use a special prefix, e.g. “//”, that indicates the paragraph isn’t to be translated. The stopwords feature mentioned above lets you automatically exempt such lines from translation. Other types of static text, you manually add to DNT.
Flagging in code
The configuration NSF for DGW contains a screen where you can enter strings that mark the beginning or end of a segment of code where all candidate strings are, or are not, to be translated. These need to be comments, so they’re different for different languages, and there may be multiple such strings per language. For instance, for macro code, at HCL we designated REM “DNT” and REM “DNT Begin” as markers for a section where nothing was to be translated, and REM “DNT End” to indicate translation could resume (which is the default state).
The DNT file is still applied to potentially stop generation of properties for strings that are in a “DNT End” zone, but of course it’s better if we can exercise discipline as developers so this doesn’t need to happen.
It’s confusing and a source of possible error to switch between translating and not-translating often in the code. Our best practice at HCL (proposed by me) was to generally try to group all the translatable strings at the top of a piece of code, then use ‘Begin DNT (or other tag appropriate to the language) just once to turn off translation for all the rest of the code in that module. If you search the Domino templates for “DNT” you can see numerous examples of this, though it hasn’t been applied with total consistency.
Warning!
Here’s a “gotcha” when using DNT tagging in code. DGW only pays attention to these tags during the key-generation phase — not during the merging-translations phase. As a result, if the exact same string appears in a DNT section and in a section that is translated, it gets translated in both places.
_types := "Report" : "Notice"; REM {DNT Begin}; @If(Form = "Report"; _types[1]; _types[2])
_types := "Informe" : "Aviso"; REM {DNT Begin}; @If(Form = "Informe"; _types[1]; _types[2])
oops overtranslation
There are two ways to solve this. DGW has a “multi keys” feature that you can use to generate separate keys for the two occurrences of “Report”. This is error prone, since if you edit the formula you might change the order of the repetitions and cause an under or over-translation.
The other way is to rewrite the formula in such a way that the identical string doesn’t appear in the DNT section. For instance:
_types := "Report" : "Notice";
REM {DNT Begin};
@If(@Lowercase(Form) = "report"; _types[1]; _types[2])
Comments for translators
Another ability DGW possesses which is underutilized, is the ability to insert comments for translators. This is done similarly to the begin and end DNT tagging, with a configurable comment string, which typically contains TSC.
'TSC Wind as in winding a clock Const LABEL_WIND = "Wind"
As shown here, when a term is potentially ambiguous, the programmer can insert a comment just before the relevant string in the code to resolve the ambiguity. The translator will be able to see this comment in the tool they use to enter translations.
Of course, for this to be useful, the programmer has to be mindful of how their strings might be interpreted differently than they intended. This is hard to do while writing them, and strings aren’t always final when they’re first entered. It’s probably best to do a separate pass for this after the code is otherwise complete.
We currently don’t use the Global Workbench, but compute the translations in formula / LS so that the user can dynamically switch languages based on a profile doc field. This works pretty well, but unfortunately there is no way to compute the view column titles in formula. That’s a missing piece that has never been added to the product.
Agreed, it would be great to have more translation support in general for “traditional” design elements. You shouldn’t need to write formulas and so on to put some static text on a form in the user’s language.
I was working on same concept of “multi-language” notes application. Here is what I have my final approach.
Maintain lotusscript library for translations
…
i18n(“lv.main.labels.reports”) = “Atskaites”
i18n(“en.main.labels.reports”) = “Reports”
…
Store them in profile document. So they become accessible from @formulas, lotusscript and java.
…
Example for computed text (pages, forms, subforms)
key:= “main.labels.reports”;
_translation_ := @GetProfileField(“frm_i18n”;”_translation_”);
result:= @Eval(_translation_);
@If(result=””;key;result)
For lotusscript:
Function lsf_TranslateTerm(term As String) As String
On Error GoTo errh
Dim macro As String
macro = {
key:= } & term & {;
_translation_ := @GetProfileField(“frm_i18n”;”_translation_”);
result:= @Eval(_translation_);
@If(result=””;key;result);
}
Dim eval As Variant
eval = Evaluate(macro)
lsf_TranslateTerm = CStr(eval(0))
errh:
Exit function
End Function
for Java
public String getTranslationFromKey(Database db, String key) {
try {
Session session = getSession();
lotus.domino.Document i18nCfg = db.getProfileDocument(“frm_i18n”, “”);
String translationFormula = “key := \”” + key + “\”; ” + i18nCfg.getItemValueString(“_translation_”);
List translationResult = session.evaluate(translationFormula);
return translationResult.get(0).toString();
} catch (NotesException e) {
e.printStackTrace();
}
return key;
}
Workaround for some parts on Notes applications are not computable or were bugged:
1) notes view columns -> i used shared columns named them in same format “main.labels.reports” then used java with dxl and parsed their names to translated ones.
2) hotspot (type button no formula there) had to use hard coded buttons and computed programmable tables that swapped to proper language tab
3) Cascaded action buttons – did not work with custom programmable labels. Formula was there but for some unknow reason it did not compute value. For this reason i had to copy-pasta them.
4) Notes view actions for unknown reason with this approach will revert translation to formula behind it if you leave it view open for some time (sometimes minutes to even hours). So i used step1 approach (sharedactions) and translated them directly with DXL.
I’ve used DGW since pre 1.0 version some almost 30 years ago.
Before that, we’d developped on other platforms and on each had a database with terms and were referencing them in the code and UI.
I’ve looked for solutions for years only to end up with the result that the only one solution that simplifies the ‘process’ of both translating AND generating a source (template) in another language is DGW.
Yes it is cluncky and has a few bugs… but it works.
I wanted to re-install it on my new machine in Win 10/Notes 12. It was not working. HCL support informed me it was not supported anymore (I called them a coule of days after June 1st 2024… end support date for Notes V9, so for DGW as well….grrr).
So I toyed around for a few days and, long story short, ended up setting up a spare laptop on Win10/Notes 9 where I had DGW running a few years ago as ‘DGW machine’. My procedures are now tested and work.
I’m still left puzzled by HCL’s decision. With tweaks, DGW could be used for any source code in Java, HTML and any text file.
From a software manufacturing process, DGW is still, as far as I know, way ahead of other alternatives.
I’d welcome any input and collaboration about the subject, as, without it, doing software in several languages becomes not economically viable…and I still want to make a decent living for a decade or so !