Internationalization (i18n)
The topic of internationalization deals largely with the translation of UI texts into different languages / locales. In this project we see three different approaches:
- message strings for web user interfaces built with the Google Web Toolkit (GWT), with i18n happening on the client based, e.g., on the browser's locale
- message strings display in the web user interface, but with the locale and message selection happening on the server side, e.g., for the Data Mining framework
- message strings for mobile apps
For all these cases, English (en) and German (de) are the development locales where development maintains the message strings. All other languages (at the time of this writing these are zh, ru, ja, es, fr, pt, it, da, sl, and cs) will be handled by SAP Language Labs (LXLabs). The process for this is explained below.
GWT UI, Messages
https://www.gwtproject.org/doc/latest/DevGuideI18nMessages.html documents the basics. We have several such messages interfaces, such as com.sap.sailing.gwt.ui.client.StringMessages in the com.sap.sailing.gwt.ui bundle. These message files contain key/value pairs where the values may contain placeholders for parameters which have to match the corresponding method signature's parameters. The message files use the general Java properties file syntax, in particular the single-quote magic for escaping character sequences which otherwise would have special meaning. Placeholders are written as the zero-based parameter number in curly braces ({0}, {1}, etc.).
For accessing these messages in client-side GWT code, obtain an instance of the messages interface, as so:
public static final StringMessages INSTANCE = GWT.create(StringMessages.class);
Backend Messages
When message strings have to be translated based on a locale already in the back-end, com.sap.sse.i18n.ResourceBundleStringMessages can be used. Create one instance like this:
private static final String STRING_MESSAGES_BASE_NAME = "stringmessages/Polars_StringMessages";
sailingServerStringMessages = new ResourceBundleStringMessagesImpl(
STRING_MESSAGES_BASE_NAME, getClassLoader(),
StandardCharsets.UTF_8.name());
The same quoting, escaping and placeholder rules apply as for standard Java properties files and hence the GWT UI messages files. The difference, when compared to the GWT UI messages, is that there is no Java interface type that models each message as a method. Instead, the default "en" locale is then provided by a file named like the base name provided to the constructor, with the .properties suffix appended. A good place is a resources/ source folder in your bundle which then contains a stringmessages sub-folder in which the .properties files reside. In the example above, the files would have paths relative to the bundle's root folder like this: resources/stringmessages/Polars_StringMessages.properties for the default "en" locale, or resources/stringmessages/Polars_StringMessages_es.properties for the Spanish locale.
The string message values are obtained in the back-end code ResourceBundleStringMessages.get methods, passing the user's locale that may, e.g., be inferred through ProxiedRemoteServiceServlet.getClientLocale() when in a GWT RPC service implementation.
Mobile Apps
We can distinguish three types of mobile apps that were created so far over the course of the program. We started by implementing a first native Android app (SAP Sailing Race Manager), using Java as the development language, sharing code also with the back-end logic. Two more apps followed with the same approach: the SAP Sailing Buoy Pinger and the self-tracking SAP Sail InSight app.
SAP Sail InSight was then also ported to iOS, using Swift / ObjectiveC as languages.
Later, SAP Sail InSight was developed again from the ground up, this time as a React Native app using TypeScript as the development language. Its naming was slightly altered into "Sail Insight (Powered by SAP)", and it went through different organizations taking care of and publishing it.
All these approaches differ in how i18n is done for them.
Native Android Apps
These keep their message strings in files called strings.xml under the res/ folder in the respective app's root folder. They are located in directories named after the locale for which they are used. The default "en" locale uses values as the directory name, so in total mobile/{app-root-folder}/res/values/strings.xml. Other locales append the locale name to the values folder name, separated by a dash. Example: mobile/{app-root-folder}/res/values-pt/strings.xml for the Portugese translations.
Old iOS App
The old iOS version of SAP Sail InSight is still around in our repository. It lives under the ios root folder in our Git workspace. For each locale there is a *.lproj sub-folder in ios/SAPTracker/src/xcode/SAPTracker where the default "en" locale is represented by the Base.lproj folder, Chinese would then be ios/SAPTracker/src/xcode/SAPTracker/zh.lproj, and so on. In each such *.lproj folder there are two files: InfoPlist.strings and Localizable.strings out of which the app draws the message strings to display for the user.
React Native Version of Sail Insight
The primary repository for the app lives at trac@sapsailing.com:/home/trac/sailinsight-git. A copy exists on Github: git@github.com:SailTracks/sailinsight.git. In order to have these be subject to translation within our existing project with SAP LXLabs, we create a synchronization job that copies the message strings from the *.json files in the src/i18n/translations directory of the app repository into the equal-named folder of our default SAP Sailing Analytics repository (currently ssh://trac@sapsailing.com/home/trac/git). When translation is completed, translation results need to be transferred back into the Sail Insight repository.
Triggering and Integrating Translations
We entertain a Git repo clone at https://github.tools.sap/SAP-Sailing-Analytics/sapsailing. When we push new commits to the translation branch of that repository (see https://github.tools.sap/SAP-Sailing-Analytics/sapsailing/tree/translation) then LXLabs will grab these changes in regular intervals (as of this writing typically every Friday evening) and look for changes in files that belong to any message collection.
Message collections are declared in the file translation_v2.json in the root of our Git workspace. Adding new collections to this file requires us to inform LXLabs (e.g., vyacheslav.sklyarenko@sap.com, called "Slava"). They need to make the corresponding update in the so-called Translation Enablement Workbench.
During the translation process, translators may have questions regarding new or changed message strings. This will results in tasks being assigned by the Translation Enablement Workbench, and an e-mail to the assignee will result. Currently, the default assignee for these tasks is axel.uhl@sap.com. We then need to answer these questions as soon as possible to unblock the translation process so that ideally we get the translation in the same sweep as those for other languages, in the same round.
When translations are done, they are pushed to the same translation branch of https://github.tools.sap/SAP-Sailing-Analytics/sapsailing by LXLabs and then usually contain the translation of all changed or added message strings. LXLabs goes by the default locale ("en") and ignores all we do in the German ("de") locale.
We have a Github webhook installed (see https://github.tools.sap/SAP-Sailing-Analytics/sapsailing/settings/hooks/285417) which triggers a CGI-BIN script that is installed on sapsailing.com at /var/www/cgi-bin/github.cgi and is versioned under configuration/httpd/cgi-bin/github.cgi to which /var/www/cgi-bin/github.cgi is a symbolic link, and which reacts to pushes from the user with the e-mail address tmsatsls+github.tools.sap_service-tip-git@sap.com, pushing to branch translation. In this case, the Git workspace installed at /home/wiki/gitwiki for the user wiki is used to fetch the latest translations, using the Git remote sapsailing which is expected to use a Personal Access Token (PAT) that is granted read access to the repository. The changes are then pushed to the translation branch on ssh://trac@sapsailing.com/home/trac/git which is expected to trigger the hudson job for the translation branch. This, in turn, will run a full build, and if successful, merge the translation branch into master and push it back to ssh://trac@sapsailing.com/home/trac/git which will then trigger the master's build job.
First building in a separate job before merging into master partly goes back to the times when LXLabs had issues exporting Java properties file syntax when placeholders and single-quote characters were used in the translations; a common problem in the French translations. However, this has improved massively, and we haven't seen any translation-incurred build errors in a long time. Yet, we stick to the process.
Roll-out to the landscape can take place after the master job has built a release successfully.
The Github webhook CGI script is pointed to by the /etc/httpd/conf.d/004-git-ssl.conf configuration which has to enable symbolic links for its CGI directory (Options FollowSymLinks). The ScriptAlias in the configuration is defined like this:
ScriptAlias /hooks/ /var/www/cgi-bin/
<Directory "/var/www/cgi-bin">
SSLOptions +StdEnvVars
AllowOverride None
Options FollowSymLinks
Order allow,deny
Allow from all
</Directory>
letting the webhook react to the URL https://git.sapsailing.com/hooks/github.cgi. The Github hook configuration must be set to use application/json as the content type as the CGI script parses pusher and ref from the request body that is expected to be in JSON format.
Adding New Collections of Translatable Strings
Follow the patterns you find in the translation_v2.json file at the root of our Git repository and make sure to inform LXLabs (currently vyacheslav.sklyarenko@sap.com, called "Slava"). Make sure you have your default ("en") and German ("de") locale-specific files in place and checked into Git. Once LXLabs has confirmed the creation of the new collection, push your changes to the https://github.tools.sap/SAP-Sailing-Analytics/sapsailing/tree/translation branch and wait for the translations to come back.
Adding Locales
This comes with two aspects.
As the first aspect, GWT requires us to "declare" the locales we support. We currently do this in two files which serve as the common ancestor in the GWT module inheritance hierarchy for the two major areas that contribute end user-facing entry points:
java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/SailingLocalesAllPermutations.gwt.xmljava/com.sap.sse.security.ui/src/main/resources/com/sap/sse/security/ui/SecurityLocalesAllPermutations.gwt.xml
They both have a sibling file ...SinglePermutation.gwt.xml which is used for local / fast builds where only one browser permutation for the default "en" locale is defined.
The list of locales supported is specified in a line like this:
<extend-property name="locale" values="en,de,zh,ru,ja,es,fr,pt,it,da,sl,cs" />
Add your locale's two-letter key to the end of this list, separated by a comma.
The second aspect is to request the translation into the new locale from LXLabs (vyacheslav.sklyarenko@sap.com, called "Slava"). This typically comes at a price tag of around 5k Euro for each new language. If the cost center owner approves, LXLabs bills the language addition to that cost center, and a few weeks later the new translations will be delivered to the translation branch.