Skip to Content
Technical Articles
Author's profile photo Gellert Lukacs

Enhance i18n resource models with recursive key references within translated texts – part 1

In this post I will propose a solution to extend the resource model to enable interesting manipulations of the translated text. In part one I would like to show the basic technique of replacing the resource model and create a mechanism which allows the use of keys inside texts. The source code provided in this post can be used as a drop-in replacement for the standard resource model.

The goal

Consider the following i18n resource bundle:keys%20within%20translated%20text

keys within translated text

When translating the key “cleverfox” by using {i18n>cleverfox} in an XMLView or when using bundle.getText("cleverfox") from JavaScript, we expect to have the following result:

The quick brown fox jumps over the lazy dog.

 

First step – extending the resource model

Before reading further, i suggest you to create a simple UI5 project and gradually include the code snippets.

Let’s start by extending the resource model and doing something fun with it.

sap.ui.define([
  "sap/ui/model/resource/ResourceModel"
  ], function(ResourceModel) {
    "use strict";

    var DeepResourceModel = ResourceModel.extend("com.sample.DeepResourceModel",  {

      getProperty: function( sPath ) {
        // call the original implementation
      	let s = ResourceModel.prototype.getProperty.call( this, sPath );
        // do a little modification
        return s.toUpperCase();
      }

    });
    return DeepResourceModel;
});

 

Copy the above code into a module in your source tree, then modify the manifest.json either by adding a new model (yellow) or changing the existing (green) one. Be aware of the namespace you use!include%20the%20new%20resource%20model%20in%20the%20manifest

include the new resource model in the manifest

After running the application you may get something similar. All the text appear in capital letters.

automatic%20capitalization%20by%20the%20resource%20model

automatic capitalization by the resource model

 

2nd step – hook into the resource bundle

Let’s see what we will get after implementing the full solution.

keys%20are%20replaced%20inside%20translatable%20texts

keys are replaced inside translatable texts

 

How resource models and resource bundles are related?

The resource model is a wrapper layer to provide binding functionality in an XMLView. The complex operations are carried out by the resource bundle. During initialization the resource model creates a resource bundle using the static method ResourceModel.loadRresourceBundle(..). After that point the requested keys are translated by referring to the getText(..) method of the resource bundle. getText(..) will take care of matching a key with a translated text.

What is the right place to hook into the i18n process?

During investigating the subject I found that there was no easy way to redefine the resource bundle. This is due to the fact that resource bundles are instantiated using static methods therefore we can’t change the classes they use. For example the loadResourceBundle method always instantiates a standard sap/base/i18n/ResourceBundle class, so we can’t insert our own.

The following approach was selected:

  1. save the original loadResourceBundle as a function pointer
  2. implement a new loadResourceBunde
  3. use the saved (original) loadResourceBundle to execute the original functionality
  4. modify the resource bundle object according to our needs.

This approach ensures that we keep full compatibility with future UI5 versions.

How to hook into getText

We use ResourceBundle.getText(..) because it is more versatile than the getProperty(..) method of the resource model. We modify the object (ResourceBundle) returned by the loadResourceBundle method.

The hook is achieved as follows:

  1. save the original getText as a function pointer within th resource bundle
  2. point the getText method to our own getText implementation
  3. bind the necessary environment to the new getText implementation
  4. make sure to call the original getText inside our implementation
  5. add any necessary extensions

 

Full source-code

Just plug this in and have some fun! You can return to the explanation a bit later.

A working prototype is available at https://github.com/gmlucas/resourcemodel_study_01.

sap.ui.define([
  "sap/ui/model/resource/ResourceModel"
  ], function(ResourceModel) {
    "use strict";

    var DeepResourceModel = ResourceModel.extend("com.sample.DeepResourceModel",  {
      constructor: function (oData) {
        oData.DeepResourceModel = this;
        ResourceModel.apply(this, arguments);
      }
    });

    const KEYREF = new RegExp(/\(\(\((.*?)\)\)\)/, 'g');

    DeepResourceModel.getText = function(sKey, aArgs, bIgnoreKeyFallback) {
      let sTemplate = this.bundle.inheritedGetText( sKey, aArgs, bIgnoreKeyFallback );
      let sTranslated = sTemplate;
      for (const captureGroup of sTemplate.matchAll( KEYREF ) ) {
      	let sub = this.bundle.getText( captureGroup[1] , [], bIgnoreKeyFallback )
        sTranslated = sTranslated.replace( captureGroup[0], sub );	
      }
      return sTranslated
    };

    ResourceModel.inheritedLoadResourceBundle = ResourceModel.loadResourceBundle;

    ResourceModel.loadResourceBundle = async function(oData, bAsync) {
      let oRB = ResourceModel.inheritedLoadResourceBundle(oData, bAsync);
      if ( bAsync ) {
        oRB = await oRB;
      }			
      if ( oData.DeepResourceModel && oRB && !oRB.inheritedGetText ) {
        oRB.inheritedGetText = oRB.getText;
        oRB.getText = DeepResourceModel
        	.getText.bind({ 
        		DeepResourceModel: oData.DeepResourceModel, 
        		bundle: oRB });
      }
      return oRB;
    }

    return DeepResourceModel;
});

 

Explaining the code

I made the source code more compact then ideal. This is because i wanted to show that there are actually not very much code to write so it must not be that complex right? Anyway, when you implement it for yourself, please make sure the right amount of comments are added.

Constructor

I call the original constructor and add an extra member to the setting object. This information (DeepResourceBundle) will be used later to provide a means for the getText method to access the resource model instance. This will be necessary for more complex scenarios.,

loadResourceBundle

We completely overwrite (destroy) the original implementation of loadResourceBundle of the ResourceModel. This is  a static method so there is no way to overwrite it in descendant classes like our DeepResourceModel. You can see in the code, that the original function is saved in inheritedLoadResourceBundle and we will use it to provide the original functionality.

We get a ResourceBundle instance. This means we will not make changes to the base class itself. We only do the hook when we are working with our new DeepResourceModel. (see the conditional statement). Remember, the loadResourceBundle is overwritten for all resource models, so even the standard ones will try to use it.

Don’t pay attention to async-await, it is required to work in al use-cases. For demo purposes i can be omitted.

The hook

In the resource bundle instance we save the original getText and then assign our implementation to it. The important thing to note here is the context we provide for this function. Normally the getText method would access the resourceBundle itself. However I wanted to give the method a way to access the DeepResourceModel which was used to create it. Therefore the bind() function will bind a structure which contains both the ResourceBundle and a reference to the DeepResourceModel.

getText with recursion

The overwritten getText() method will be called by the framework or by JavaScript. As the function is bound to a special object containing the resource model and the resource bundle, the original getText implementation can be accessed through: this.bundle.inheritedGetText().

This provides us with the basic hook mechanism. We call the original and return the results.

Now the recursion and key processing:

When the normal translated text is returned (sTemplate) it may contain references to other keys. For example: (((attitude))). The keys are extracted by a regular expression and each of them is processed. This is where recursion comes handy.  We call ourselves but this time only using the key “attitude” to get the results. When the function returns it will contain “lazy” which is than substituted for the key.

Do not hesitate to add some console.logs to the code and see what is happening. Even deep references can be used.

 

Where do we go from here?

By being able to change how a resource bundle is working we can do things like:

  • replace words on the fly
  • hide numbers
  • provide translations by an odata service
  • add invisible ‘watermarks’ to string elements on the screen
  • allow the user to change translations on the fly

 

I look forward to explore all of the above in future posts.

 

If you want to go deeper, you can always check how UI5 works by browsing the openUI5 repository. I include two source files which are relevant for this use case.

https://github.com/SAP/openui5/blob/master/src/sap.ui.core/src/sap/ui/model/resource/ResourceModel.js

https://github.com/SAP/openui5/blob/master/src/sap.ui.core/src/sap/base/i18n/ResourceBundle.js

 

Assigned Tags

      1 Comment
      You must be Logged on to comment or reply to a post.
      Author's profile photo Gellert Lukacs
      Gellert Lukacs
      Blog Post Author

      Quick note: in the shared repositories the source may be slightly different. This is because the full solution requires somewhat more complex techniques. In some cases a better solution is found and included. I will be glad to incorporate your feedback in the code as well, so keep tuned.

      Ge.