Technology Blogs by Members
Explore a vibrant mix of technical expertise, industry insights, and tech buzz in member blogs covering SAP products, technology, and events. Get in the mix!
cancel
Showing results for 
Search instead for 
Did you mean: 
gmlucas
Explorer
In this short blog-post i would like to show how I used the ResourceModel to allow the business user to interact with the translated text stored in i18n property files.


Bring up the UI with ctrl+alt+shift+T


Lately I set out to explore how one could integrate a whole application into another with as less intervention to the original as possible. The problem arose from a need to provide client-side translations to business users who were supposed to translate the applications on the fly without having to interact with the developer team. It was sure that at one point this system will have to use its own user interface. As the number of applications for such a scenario could be quite large - hence the need to translate without the dev team - the solution could not be invasive.

In part-1 we saw how the ResourceModel can be used to alter the way i18n translations worked.

Now it's time to use the ResourceModel for something even more crazy!

The goal



  • Integrate a small user interface into the model

  • Do not change the host application while adding the UI

  • Control how the resource model works through the embedded user interface


We will allow the user to press a key combination to bring up a small menu to control how the translated sentences look inside the host application. As you see on the intro animation, when the user presses ctrl+alt+shift+T the menu appears. On the menu simple settings are made to change the output the resource model provides. What the resource and the user interface does is completely transparent from the point of view of the host application.

Step 1 - add a key shortcut


As a first step let's separate the concerns into the following modules:

  1. The usual ResourceModel.js will be the entry point to the application and it will handle how the translated text looks. Also, it will manage the persistence of the state set by the user.

  2. The UserInterface.js will be responsible for the actual display of UI elements and interactions with the user.


When the host application starts it will initialize the resource model provided in the manifest.json file. Since this file is our new Enhanced Resource Model, the constructor is an ideal place to execute any kind of initialization required by the app.

So In the constructor, initialize the UserInterface module by passing the pointer of the current Resource Model.

In the user interface we Install the keypress event handler.

The application will react to the following combination:ctrl+alt+shift+T

Step 2 - display a menu without affecting the host app


UI5 has a built in helper class named sap/ui/core/Popup which allows the creation of popup elements independently of other controls. I created a popup then assigned a Fragment to it. To avoid external file operations the fragment was defined inside the JS code and contains a Popover control.


Menu appears over the host application


When the magic key combination is detected, we will display the user interface as follows:

  • create Popup control

  • load a fragment definition

  • assign the fragment to the popup

  • assign and bind models to the fragment

  • open the popup.


The menu will open over the actual content of the application screen,

Step 3 - implement Resource Model functionality


The resource model will change how the text looks-like in the original application. For this demo, i created 3 simple rules:

  • display text in uppercase letters

  • remove numbers (for example salary figures) from the text

  • standard display


As the framework uses the getProperty method of the ResourceModel to provide a translated text for a given key, the getProperty method was the perfect place to alter the output.

Step 4 - restore data after restarting the browser


A key property of this custom solution is the ability to disable itself. This is because the "translation feature" is only meant for power users and only during specific times. When the added functionality is not needed it is best if the new ResourceModel doesn't interfere with the standard at all.

A convenient way to store data across sessions is the browsers "LocalStorage". This can be easily accessed through a UI5 helper library called: sap/ui/util/Storage.

Two properties are stored:

  • the enabled status of the ResourceModel

  • the transformation mode selected by the use.



Data stored in the local storage


Each change on the user interface is written to the local storage and at each application startup the data is retrieved from the local storage.

 

The source code


The source code is also available at https://github.com/gmlucas/resourcemodel_study_02

The ResourceModel.js
sap.ui.define([
"sap/ui/model/resource/ResourceModel",
"sap/ui/util/Storage",
"./UserInterface"
], function (ResourceModel, LocalStorage, UserInterface ) {
"use strict";

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

status: { enabled: false, transform: "none" },

constructor: function (oData) {
ResourceModel.apply(this, arguments);
this.status = this.getStatus();
UserInterface.init(this);
},

getProperty: function( sPath ) {
let s = ResourceModel.prototype.getProperty.call( this, sPath );
if ( this.status.enabled ) {
this.injectGetText();
switch ( this.status.transform ) {
case "upper":
s = s.toUpperCase();
break;
case "hidenum":
s = s.replaceAll(/(\d{1,15}\.?\d{1,15})/g,"****");
break;
}
}
return s;
},

getStatus: function() {
let storage = new LocalStorage(LocalStorage.Type.local, "tr");
let storageKey = "status-"+this.oData.bundleName;
let status = JSON.parse(storage.get( storageKey )) || { "enabled": false };
return status;
},

saveStatus: function() {
let storage = new LocalStorage(LocalStorage.Type.local, "tr");
let storageKey = "status-"+this.oData.bundleName;
storage.put( storageKey, JSON.stringify( this.status ) );
},

setEnabled: function( isEnabled ) {
this.status.enabled = isEnabled;
this.saveStatus();
this.refresh(true);
},

setTransformation: function( transformation ) {
this.status.transform = transformation;
this.saveStatus();
this.refresh(true);
},

injectGetText: function() {
let oRB = this.getResourceBundle();
if ( oRB && !oRB.inheritedGetText ) {
oRB.inheritedGetText = oRB.getText;
oRB.getText = DeepResourceModel.getText.bind({ DeepResourceModel: this, bundle: oRB });
}
}
});

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

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

return DeepResourceModel;
});

The UserInterface.js
sap.ui.define([ 
"sap/ui/core/Fragment",
"sap/ui/core/Popup"
], function( Fragment, Popup ) {
"use strict";

var UserInterface = {

_oPopup: null,

init: function( resourceModel ) {
document.addEventListener("keydown", this.handleKeypress.bind(this), { capture: true } );
this.resourceModel = resourceModel;
this.UI = new sap.ui.model.json.JSONModel( this.defineModelData() );
},

defineModelData: function() {
return {
enabled: false,
transform: "none"
}
},

handleKeypress: function( ev ) {
if ( ev.keyCode === 84 ) {
if ( ev.altKey && ev.ctrlKey && ev.shiftKey ) {
ev.stopImmediatePropagation();
this.openTranslationMenu();
}
}
},

openTranslationMenu: function() {
if ( !this._oPopup ) {
this._oPopup = new Popup(this, true, false, false );
}
if ( this._oPopup && !this._oPopup.isOpen() ) {
Fragment.load({
type: 'XML', definition: MENU_FRAGMENT,
controller: this,
id: "translation-frame-menu"
}).then(function( fragment ){
fragment.setModel( this.UI, "UI" );
this.UI.setProperty("/enabled", this.resourceModel.status.enabled );
this.UI.setProperty("/transform", this.resourceModel.status.transform );
this._oPopup.setContent( fragment );
this._oPopup.open(1, sap.ui.core.Popup.Dock.EndTop, sap.ui.core.Popup.Dock.EndTop, document, '-32 32', "none");
}.bind(this));
}
},

handleCloseMenu: function() {
if ( this._oPopup !== null ) {
this._oPopup.close(0);
}
},

handleSwitch: function( oEvent ) {
this.resourceModel.setEnabled( this.UI.getProperty("/enabled") );
},

handleTransformationChange: function( oEvent ) {
this.resourceModel.setTransformation( this.UI.getProperty("/transform") );
}

};

/* ==========================================================
* Inline XML fragment definition
* ========================================================== */

let MENU_FRAGMENT =
`<core:FragmentDefinition xmlns:html="http://www.w3.org/1999/xhtml" xmlns="sap.m" xmlns:core="sap.ui.core">
<Popover title="Translation Experiments" class="sapUiSizeCompact" contentMinWidth="28rem">
<VBox>
<InputListItem label="Local Translation Support:" class="sapUiTinyMargin">
<Switch state="{UI>/enabled}" change="handleSwitch"/>
</InputListItem>
<InputListItem label="Text transformation:" class="sapUiTinyMargin">
<Select selectedKey="{UI>/transform}" enabled="{UI>/enabled}" change="handleTransformationChange">
<core:Item key="none" text="None"/>
<core:Item key="upper" text="Upper case"/>
<core:Item key="hidenum" text="Hide numbers"/>
</Select>
</InputListItem>
</VBox>
<endButton>
<Button icon="sap-icon://decline" press="handleCloseMenu"/>
</endButton>
</Popover>
</core:FragmentDefinition>`;

return UserInterface;
});

 

Conclusion


Currently two technologies were merged:

  • the ability to control what the ResourceModel emits for a given i18n key

  • the ability to include a user interface into an app without having to change views or controllers


With this knowledge we slowly approach the point where we could:

  • provide a UI where power users can change the text assigned to an i18n label

  • keep the changed text and show it in place of the original i18n text

  • export the changed text in a format useful for application developers


Later I would like to explore the following:

  • How an i18n text can be identified on the screen

  • How to find the text associated with a label by clicking on the screen

  • How to provide the translation for the developers

  • How to make the translations visible without re-compiling the app


 

If you made it so far, thanks for you patience. Please let me know you thoughts and ideas in the comment section.
2 Comments
Labels in this area