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

Enhance i18n resource models with a User Interface to allow client-side translations – part 2

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%20up%20the%20UI%20with%20ctrl+alt+shift+T

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%20appears%20over%20the%20host%20application

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%20stored%20in%20the%20local%20storage

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.

Assigned Tags

      2 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Shai Sinai
      Shai Sinai

      A very impressive concept.

      Author's profile photo Gellert Lukacs
      Gellert Lukacs
      Blog Post Author

      Thanks, it means a lot to have my first ever reply 🙂 I do plan to explore this l'art pour l'art topic deeper in future posts. Ge.